mirror of
https://github.com/processone/ejabberd.git
synced 2024-06-26 22:35:31 +02:00
35a076c251
Since now, ejabberd doesn't ignore unknown options and doesn't allow to have options with malformed values. The rationale for this is to avoid unexpected behaviour during runtime, i.e. to conform to "fail early" approach. Note that it's safe to reload a configuration with potentialy invalid and/or unknown options: this will not halt ejabberd, but will only prevent the configuration from loading. ***NOTE FOR PACKAGE BUILDERS*** This new behaviour should be documented in the upgrade notes.
1498 lines
49 KiB
Erlang
1498 lines
49 KiB
Erlang
%%%----------------------------------------------------------------------
|
|
%%% File : ejabberd_config.erl
|
|
%%% Author : Alexey Shchepin <alexey@process-one.net>
|
|
%%% Purpose : Load config file
|
|
%%% Created : 14 Dec 2002 by Alexey Shchepin <alexey@process-one.net>
|
|
%%%
|
|
%%%
|
|
%%% ejabberd, Copyright (C) 2002-2018 ProcessOne
|
|
%%%
|
|
%%% This program is free software; you can redistribute it and/or
|
|
%%% modify it under the terms of the GNU General Public License as
|
|
%%% published by the Free Software Foundation; either version 2 of the
|
|
%%% License, or (at your option) any later version.
|
|
%%%
|
|
%%% This program is distributed in the hope that it will be useful,
|
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
%%% General Public License for more details.
|
|
%%%
|
|
%%% You should have received a copy of the GNU General Public License along
|
|
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
%%%
|
|
%%%----------------------------------------------------------------------
|
|
|
|
-module(ejabberd_config).
|
|
-author('alexey@process-one.net').
|
|
|
|
-export([start/0, load_file/1, reload_file/0, read_file/1,
|
|
get_option/1, get_option/2, add_option/2, has_option/1,
|
|
get_version/0, get_myhosts/0, get_mylang/0, get_lang/1,
|
|
get_ejabberd_config_path/0, is_using_elixir_config/0,
|
|
prepare_opt_val/4, transform_options/1, collect_options/1,
|
|
convert_to_yaml/1, convert_to_yaml/2, v_db/2,
|
|
env_binary_to_list/2, opt_type/1, may_hide_data/1,
|
|
is_elixir_enabled/0, v_dbs/1, v_dbs_mods/1,
|
|
default_db/1, default_db/2, default_ram_db/1, default_ram_db/2,
|
|
default_queue_type/1, queue_dir/0, fsm_limit_opts/1,
|
|
use_cache/1, cache_size/1, cache_missed/1, cache_life_time/1,
|
|
codec_options/1, get_plain_terms_file/2, negotiation_timeout/0]).
|
|
|
|
-export([start/2]).
|
|
|
|
%% The following functions are deprecated.
|
|
-export([add_global_option/2, add_local_option/2,
|
|
get_global_option/2, get_local_option/2,
|
|
get_global_option/3, get_local_option/3,
|
|
get_option/3]).
|
|
-export([is_file_readable/1]).
|
|
|
|
-deprecated([{add_global_option, 2}, {add_local_option, 2},
|
|
{get_global_option, 2}, {get_local_option, 2},
|
|
{get_global_option, 3}, {get_local_option, 3},
|
|
{get_option, 3}, {is_file_readable, 1}]).
|
|
|
|
-include("ejabberd.hrl").
|
|
-include("logger.hrl").
|
|
-include("ejabberd_config.hrl").
|
|
-include_lib("kernel/include/file.hrl").
|
|
-include_lib("stdlib/include/ms_transform.hrl").
|
|
|
|
-callback opt_type(atom()) -> function() | [atom()].
|
|
-type bad_option() :: invalid_option | unknown_option.
|
|
|
|
-spec start() -> ok | {error, bad_option()}.
|
|
start() ->
|
|
ConfigFile = get_ejabberd_config_path(),
|
|
?INFO_MSG("Loading configuration from ~s", [ConfigFile]),
|
|
catch ets:new(ejabberd_options,
|
|
[named_table, public, {read_concurrency, true}]),
|
|
catch ets:new(ejabberd_db_modules,
|
|
[named_table, public, {read_concurrency, true}]),
|
|
case load_file(ConfigFile) of
|
|
{ok, State1} ->
|
|
UnixTime = p1_time_compat:system_time(seconds),
|
|
SharedKey = case erlang:get_cookie() of
|
|
nocookie ->
|
|
str:sha(randoms:get_string());
|
|
Cookie ->
|
|
str:sha(misc:atom_to_binary(Cookie))
|
|
end,
|
|
State2 = set_option({node_start, global}, UnixTime, State1),
|
|
State3 = set_option({shared_key, global}, SharedKey, State2),
|
|
set_opts(State3),
|
|
ok;
|
|
{error, _} = Err ->
|
|
?ERROR_MSG("Failed to load configuration file ~s", [ConfigFile]),
|
|
Err
|
|
end.
|
|
|
|
%% When starting ejabberd for testing, we sometimes want to start a
|
|
%% subset of hosts from the one define in the config file.
|
|
%% This function override the host list read from config file by the
|
|
%% one we provide.
|
|
%% Hosts to start are defined in an ejabberd application environment
|
|
%% variable 'hosts' to make it easy to ignore some host in config
|
|
%% file.
|
|
hosts_to_start(State) ->
|
|
case application:get_env(ejabberd, hosts) of
|
|
undefined ->
|
|
%% Start all hosts as defined in config file
|
|
State;
|
|
{ok, Hosts} ->
|
|
set_hosts_in_options(Hosts, State)
|
|
end.
|
|
|
|
%% @private
|
|
%% At the moment, these functions are mainly used to setup unit tests.
|
|
-spec start(Hosts :: [binary()], Opts :: [acl:acl() | local_config()]) -> ok.
|
|
start(Hosts, Opts) ->
|
|
catch ets:new(ejabberd_options,
|
|
[named_table, public, {read_concurrency, true}]),
|
|
catch ets:new(ejabberd_db_modules,
|
|
[named_table, public, {read_concurrency, true}]),
|
|
UnixTime = p1_time_compat:system_time(seconds),
|
|
SharedKey = case erlang:get_cookie() of
|
|
nocookie ->
|
|
str:sha(randoms:get_string());
|
|
Cookie ->
|
|
str:sha(misc:atom_to_binary(Cookie))
|
|
end,
|
|
State1 = #state{opts = Opts},
|
|
State2 = set_option({node_start, global}, UnixTime, State1),
|
|
State3 = set_option({shared_key, global}, SharedKey, State2),
|
|
set_opts(set_hosts_in_options(Hosts, State3)),
|
|
ok.
|
|
|
|
%% @doc Get the filename of the ejabberd configuration file.
|
|
%% The filename can be specified with: erl -config "/path/to/ejabberd.yml".
|
|
%% It can also be specified with the environtment variable EJABBERD_CONFIG_PATH.
|
|
%% If not specified, the default value 'ejabberd.yml' is assumed.
|
|
%% @spec () -> string()
|
|
get_ejabberd_config_path() ->
|
|
case get_env_config() of
|
|
{ok, Path} -> Path;
|
|
undefined ->
|
|
case os:getenv("EJABBERD_CONFIG_PATH") of
|
|
false ->
|
|
?CONFIG_PATH;
|
|
Path ->
|
|
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".
|
|
case application:get_env(ejabberd, config) of
|
|
R = {ok, _Path} -> R;
|
|
undefined ->
|
|
%% Second case for embbeding ejabberd in another app, for example for Elixir:
|
|
%% config :ejabberd,
|
|
%% file: "config/ejabberd.yml"
|
|
application:get_env(ejabberd, file)
|
|
end.
|
|
|
|
%% @doc Read the ejabberd configuration file.
|
|
%% It also includes additional configuration files and replaces macros.
|
|
%% This function will crash if finds some error in the configuration file.
|
|
%% @spec (File::string()) -> #state{}
|
|
read_file(File) ->
|
|
read_file(File, [{replace_macros, true},
|
|
{include_files, true},
|
|
{include_modules_configs, true}]).
|
|
|
|
read_file(File, Opts) ->
|
|
Terms1 = case is_elixir_enabled() of
|
|
true ->
|
|
case 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(File) of
|
|
true ->
|
|
'Elixir.Ejabberd.Config':init(File),
|
|
'Elixir.Ejabberd.Config':get_ejabberd_opts();
|
|
false ->
|
|
get_plain_terms_file(File, Opts)
|
|
end;
|
|
false ->
|
|
get_plain_terms_file(File, Opts)
|
|
end,
|
|
Terms_macros = case proplists:get_bool(replace_macros, Opts) of
|
|
true -> replace_macros(Terms1);
|
|
false -> Terms1
|
|
end,
|
|
Terms = transform_terms(Terms_macros),
|
|
State = lists:foldl(fun search_hosts/2, #state{}, Terms),
|
|
{Head, Tail} = lists:partition(
|
|
fun({host_config, _}) -> false;
|
|
({append_host_config, _}) -> false;
|
|
(_) -> true
|
|
end, Terms),
|
|
State1 = lists:foldl(fun process_term/2, State, Head ++ Tail),
|
|
State1#state{opts = compact(State1#state.opts)}.
|
|
|
|
-spec load_file(string()) -> {ok, #state{}} | {error, bad_option()}.
|
|
|
|
load_file(File) ->
|
|
State0 = read_file(File),
|
|
State1 = hosts_to_start(State0),
|
|
AllMods = get_modules(),
|
|
init_module_db_table(AllMods),
|
|
ModOpts = get_modules_with_options(AllMods),
|
|
validate_opts(State1, ModOpts).
|
|
|
|
-spec reload_file() -> ok | {error, bad_option()}.
|
|
|
|
reload_file() ->
|
|
Config = get_ejabberd_config_path(),
|
|
OldHosts = get_myhosts(),
|
|
case load_file(Config) of
|
|
{error, _} = Err ->
|
|
Err;
|
|
{ok, State} ->
|
|
set_opts(State),
|
|
NewHosts = get_myhosts(),
|
|
lists:foreach(
|
|
fun(Host) ->
|
|
ejabberd_hooks:run(host_up, [Host])
|
|
end, NewHosts -- OldHosts),
|
|
lists:foreach(
|
|
fun(Host) ->
|
|
ejabberd_hooks:run(host_down, [Host])
|
|
end, OldHosts -- NewHosts),
|
|
ejabberd_hooks:run(config_reloaded, [])
|
|
end.
|
|
|
|
-spec convert_to_yaml(file:filename()) -> ok | {error, any()}.
|
|
|
|
convert_to_yaml(File) ->
|
|
convert_to_yaml(File, stdout).
|
|
|
|
-spec convert_to_yaml(file:filename(),
|
|
stdout | file:filename()) -> ok | {error, any()}.
|
|
|
|
convert_to_yaml(File, Output) ->
|
|
State = read_file(File, [{include_files, false}]),
|
|
Opts = [{K, V} || #local_config{key = K, value = V} <- State#state.opts],
|
|
{GOpts, HOpts} = split_by_hosts(Opts),
|
|
NewOpts = GOpts ++ lists:map(
|
|
fun({Host, Opts1}) ->
|
|
{host_config, [{Host, Opts1}]}
|
|
end, HOpts),
|
|
Data = fast_yaml:encode(lists:reverse(NewOpts)),
|
|
case Output of
|
|
stdout ->
|
|
io:format("~s~n", [Data]);
|
|
FileName ->
|
|
file:write_file(FileName, Data)
|
|
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.
|
|
-spec env_binary_to_list(atom(), atom()) -> {ok, any()}|undefined.
|
|
env_binary_to_list(Application, Parameter) ->
|
|
%% Application need to be loaded to allow setting parameters
|
|
application:load(Application),
|
|
case application:get_env(Application, Parameter) of
|
|
{ok, Val} when is_binary(Val) ->
|
|
BVal = binary_to_list(Val),
|
|
application:set_env(Application, Parameter, BVal),
|
|
{ok, BVal};
|
|
Other ->
|
|
Other
|
|
end.
|
|
|
|
%% @doc Read an ejabberd configuration file and return the terms.
|
|
%% Input is an absolute or relative path to an ejabberd config file.
|
|
%% Returns a list of plain terms,
|
|
%% in which the options 'include_config_file' were parsed
|
|
%% and the terms in those files were included.
|
|
%% @spec(iolist()) -> [term()]
|
|
get_plain_terms_file(File, Opts) when is_binary(File) ->
|
|
get_plain_terms_file(binary_to_list(File), Opts);
|
|
get_plain_terms_file(File1, Opts) ->
|
|
File = get_absolute_path(File1),
|
|
DontStopOnError = lists:member(dont_halt_on_error, Opts),
|
|
case consult(File) of
|
|
{ok, Terms} ->
|
|
BinTerms1 = strings_to_binary(Terms),
|
|
ModInc = case proplists:get_bool(include_modules_configs, Opts) of
|
|
true ->
|
|
Files = [{filename:rootname(filename:basename(F)), F}
|
|
|| F <- filelib:wildcard(ext_mod:config_dir() ++ "/*.{yml,yaml}")
|
|
++ filelib:wildcard(ext_mod:modules_dir() ++ "/*/conf/*.{yml,yaml}")],
|
|
[proplists:get_value(F,Files) || F <- proplists:get_keys(Files)];
|
|
_ ->
|
|
[]
|
|
end,
|
|
BinTerms = BinTerms1 ++ [{include_config_file, list_to_binary(V)} || V <- ModInc],
|
|
case proplists:get_bool(include_files, Opts) of
|
|
true ->
|
|
include_config_files(BinTerms);
|
|
false ->
|
|
BinTerms
|
|
end;
|
|
{error, enoent, Reason} ->
|
|
case DontStopOnError of
|
|
true ->
|
|
?WARNING_MSG(Reason, []),
|
|
[];
|
|
_ ->
|
|
?ERROR_MSG(Reason, []),
|
|
exit_or_halt(Reason)
|
|
end;
|
|
{error, Reason} ->
|
|
?ERROR_MSG(Reason, []),
|
|
case DontStopOnError of
|
|
true -> [];
|
|
_ -> exit_or_halt(Reason)
|
|
end
|
|
end.
|
|
|
|
consult(File) ->
|
|
case filename:extension(File) of
|
|
Ex when (Ex == ".yml") or (Ex == ".yaml") ->
|
|
case fast_yaml:decode_from_file(File, [plain_as_atom]) of
|
|
{ok, []} ->
|
|
{ok, []};
|
|
{ok, [Document|_]} ->
|
|
{ok, parserl(Document)};
|
|
{error, Err} ->
|
|
Msg1 = "Cannot load " ++ File ++ ": ",
|
|
Msg2 = fast_yaml:format_error(Err),
|
|
case Err of
|
|
enoent ->
|
|
{error, enoent, Msg1 ++ Msg2};
|
|
_ ->
|
|
{error, Msg1 ++ Msg2}
|
|
end
|
|
end;
|
|
_ ->
|
|
case file:consult(File) of
|
|
{ok, Terms} ->
|
|
{ok, Terms};
|
|
{error, {LineNumber, erl_parse, _ParseMessage} = Reason} ->
|
|
{error, describe_config_problem(File, Reason, LineNumber)};
|
|
{error, Reason} ->
|
|
case Reason of
|
|
enoent ->
|
|
{error, enoent, describe_config_problem(File, Reason)};
|
|
_ ->
|
|
{error, describe_config_problem(File, Reason)}
|
|
end
|
|
end
|
|
end.
|
|
|
|
parserl(<<"> ", Term/binary>>) ->
|
|
{ok, A2, _} = erl_scan:string(binary_to_list(Term)),
|
|
{ok, A3} = erl_parse:parse_term(A2),
|
|
A3;
|
|
parserl({A, B}) ->
|
|
{parserl(A), parserl(B)};
|
|
parserl([El|Tail]) ->
|
|
[parserl(El) | parserl(Tail)];
|
|
parserl(Other) ->
|
|
Other.
|
|
|
|
%% @doc Convert configuration filename to absolute path.
|
|
%% Input is an absolute or relative path to an ejabberd configuration file.
|
|
%% And returns an absolute path to the configuration file.
|
|
%% @spec (string()) -> string()
|
|
get_absolute_path(File) ->
|
|
case filename:pathtype(File) of
|
|
absolute ->
|
|
File;
|
|
relative ->
|
|
{ok, Dir} = file:get_cwd(),
|
|
filename:absname_join(Dir, File);
|
|
volumerelative ->
|
|
filename:absname(File)
|
|
end.
|
|
|
|
search_hosts(Term, State) ->
|
|
case Term of
|
|
{host, Host} ->
|
|
if
|
|
State#state.hosts == [] ->
|
|
set_hosts_in_options([Host], State);
|
|
true ->
|
|
?ERROR_MSG("Can't load config file: "
|
|
"too many hosts definitions", []),
|
|
exit("too many hosts definitions")
|
|
end;
|
|
{hosts, Hosts} ->
|
|
if
|
|
State#state.hosts == [] ->
|
|
set_hosts_in_options(Hosts, State);
|
|
true ->
|
|
?ERROR_MSG("Can't load config file: "
|
|
"too many hosts definitions", []),
|
|
exit("too many hosts definitions")
|
|
end;
|
|
_ ->
|
|
State
|
|
end.
|
|
|
|
set_hosts_in_options(Hosts, State) ->
|
|
PrepHosts = normalize_hosts(Hosts),
|
|
NewOpts = lists:filter(fun({local_config,{hosts,global},_}) -> false;
|
|
(_) -> true
|
|
end, State#state.opts),
|
|
set_option({hosts, global}, PrepHosts, State#state{hosts = PrepHosts, opts = NewOpts}).
|
|
|
|
normalize_hosts(Hosts) ->
|
|
normalize_hosts(Hosts,[]).
|
|
normalize_hosts([], PrepHosts) ->
|
|
lists:reverse(PrepHosts);
|
|
normalize_hosts([Host|Hosts], PrepHosts) ->
|
|
case jid:nodeprep(iolist_to_binary(Host)) of
|
|
error ->
|
|
?ERROR_MSG("Can't load config file: "
|
|
"invalid host name [~p]", [Host]),
|
|
exit("invalid hostname");
|
|
PrepHost ->
|
|
normalize_hosts(Hosts, [PrepHost|PrepHosts])
|
|
end.
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%% Errors reading the config file
|
|
|
|
describe_config_problem(Filename, Reason) ->
|
|
Text1 = lists:flatten("Problem loading ejabberd config file " ++ Filename),
|
|
Text2 = lists:flatten(" : " ++ file:format_error(Reason)),
|
|
ExitText = Text1 ++ Text2,
|
|
ExitText.
|
|
|
|
describe_config_problem(Filename, Reason, LineNumber) ->
|
|
Text1 = lists:flatten("Problem loading ejabberd config file " ++ Filename),
|
|
Text2 = lists:flatten(" approximately in the line "
|
|
++ file:format_error(Reason)),
|
|
ExitText = Text1 ++ Text2,
|
|
Lines = get_config_lines(Filename, LineNumber, 10, 3),
|
|
?ERROR_MSG("The following lines from your configuration file might be"
|
|
" relevant to the error: ~n~s", [Lines]),
|
|
ExitText.
|
|
|
|
get_config_lines(Filename, TargetNumber, PreContext, PostContext) ->
|
|
{ok, Fd} = file:open(Filename, [read]),
|
|
LNumbers = lists:seq(TargetNumber-PreContext, TargetNumber+PostContext),
|
|
NextL = io:get_line(Fd, no_prompt),
|
|
R = get_config_lines2(Fd, NextL, 1, LNumbers, []),
|
|
file:close(Fd),
|
|
R.
|
|
|
|
get_config_lines2(_Fd, eof, _CurrLine, _LNumbers, R) ->
|
|
lists:reverse(R);
|
|
get_config_lines2(_Fd, _NewLine, _CurrLine, [], R) ->
|
|
lists:reverse(R);
|
|
get_config_lines2(Fd, Data, CurrLine, [NextWanted | LNumbers], R) when is_list(Data) ->
|
|
NextL = io:get_line(Fd, no_prompt),
|
|
if
|
|
CurrLine >= NextWanted ->
|
|
Line2 = [integer_to_list(CurrLine), ": " | Data],
|
|
get_config_lines2(Fd, NextL, CurrLine+1, LNumbers, [Line2 | R]);
|
|
true ->
|
|
get_config_lines2(Fd, NextL, CurrLine+1, [NextWanted | LNumbers], R)
|
|
end.
|
|
|
|
%% If ejabberd isn't yet running in this node, then halt the node
|
|
exit_or_halt(ExitText) ->
|
|
case [Vsn || {ejabberd, _Desc, Vsn} <- application:which_applications()] of
|
|
[] ->
|
|
ejabberd:halt();
|
|
[_] ->
|
|
exit(ExitText)
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%% Support for 'include_config_file'
|
|
|
|
get_config_option_key(Name, Val) ->
|
|
if Name == listen ->
|
|
case Val of
|
|
{{Port, IP, Trans}, _Mod, _Opts} ->
|
|
{Port, IP, Trans};
|
|
{{Port, Trans}, _Mod, _Opts} when Trans == tcp; Trans == udp ->
|
|
{Port, {0,0,0,0}, Trans};
|
|
{{Port, IP}, _Mod, _Opts} ->
|
|
{Port, IP, tcp};
|
|
{Port, _Mod, _Opts} ->
|
|
{Port, {0,0,0,0}, tcp};
|
|
V when is_list(V) ->
|
|
lists:foldl(
|
|
fun({port, Port}, {_, IP, T}) ->
|
|
{Port, IP, T};
|
|
({ip, IP}, {Port, _, T}) ->
|
|
{Port, IP, T};
|
|
({transport, T}, {Port, IP, _}) ->
|
|
{Port, IP, T};
|
|
(_, Res) ->
|
|
Res
|
|
end, {5222, {0,0,0,0}, tcp}, Val)
|
|
end;
|
|
is_tuple(Val) ->
|
|
element(1, Val);
|
|
true ->
|
|
Val
|
|
end.
|
|
|
|
maps_to_lists(IMap) ->
|
|
maps:fold(fun(Name, Map, Res) when Name == host_config orelse Name == append_host_config ->
|
|
[{Name, [{Host, maps_to_lists(SMap)} || {Host,SMap} <- maps:values(Map)]} | Res];
|
|
(Name, Map, Res) when is_map(Map) ->
|
|
[{Name, maps:values(Map)} | Res];
|
|
(Name, Val, Res) ->
|
|
[{Name, Val} | Res]
|
|
end, [], IMap).
|
|
|
|
merge_configs(Terms, ResMap) ->
|
|
lists:foldl(fun({Name, Val}, Map) when is_list(Val), Name =/= auth_method ->
|
|
Old = maps:get(Name, Map, #{}),
|
|
New = lists:foldl(fun(SVal, OMap) ->
|
|
NVal = if Name == host_config orelse Name == append_host_config ->
|
|
{Host, Opts} = SVal,
|
|
{_, SubMap} = maps:get(Host, OMap, {Host, #{}}),
|
|
{Host, merge_configs(Opts, SubMap)};
|
|
true ->
|
|
SVal
|
|
end,
|
|
maps:put(get_config_option_key(Name, SVal), NVal, OMap)
|
|
end, Old, Val),
|
|
maps:put(Name, New, Map);
|
|
({Name, Val}, Map) ->
|
|
maps:put(Name, Val, Map)
|
|
end, ResMap, Terms).
|
|
|
|
%% @doc Include additional configuration files in the list of terms.
|
|
%% @spec ([term()]) -> [term()]
|
|
include_config_files(Terms) ->
|
|
{FileOpts, Terms1} =
|
|
lists:mapfoldl(
|
|
fun({include_config_file, _} = T, Ts) ->
|
|
{[transform_include_option(T)], Ts};
|
|
({include_config_file, _, _} = T, Ts) ->
|
|
{[transform_include_option(T)], Ts};
|
|
(T, Ts) ->
|
|
{[], [T|Ts]}
|
|
end, [], Terms),
|
|
Terms2 = lists:flatmap(
|
|
fun({File, Opts}) ->
|
|
include_config_file(File, Opts)
|
|
end, lists:flatten(FileOpts)),
|
|
|
|
M1 = merge_configs(Terms1, #{}),
|
|
M2 = merge_configs(Terms2, M1),
|
|
maps_to_lists(M2).
|
|
|
|
transform_include_option({include_config_file, File}) when is_list(File) ->
|
|
case is_string(File) of
|
|
true -> {File, []};
|
|
false -> File
|
|
end;
|
|
transform_include_option({include_config_file, Filename}) ->
|
|
{Filename, []};
|
|
transform_include_option({include_config_file, Filename, Options}) ->
|
|
{Filename, Options}.
|
|
|
|
include_config_file(Filename, Options) ->
|
|
Included_terms = get_plain_terms_file(Filename, [{include_files, true}, dont_halt_on_error]),
|
|
Disallow = proplists:get_value(disallow, Options, []),
|
|
Included_terms2 = delete_disallowed(Disallow, Included_terms),
|
|
Allow_only = proplists:get_value(allow_only, Options, all),
|
|
keep_only_allowed(Allow_only, Included_terms2).
|
|
|
|
%% @doc Filter from the list of terms the disallowed.
|
|
%% Returns a sublist of Terms without the ones which first element is
|
|
%% included in Disallowed.
|
|
%% @spec (Disallowed::[atom()], Terms::[term()]) -> [term()]
|
|
delete_disallowed(Disallowed, Terms) ->
|
|
lists:foldl(
|
|
fun(Dis, Ldis) ->
|
|
delete_disallowed2(Dis, Ldis)
|
|
end,
|
|
Terms,
|
|
Disallowed).
|
|
|
|
delete_disallowed2(Disallowed, [H|T]) ->
|
|
case element(1, H) of
|
|
Disallowed ->
|
|
?WARNING_MSG("The option '~p' is disallowed, "
|
|
"and will not be accepted", [Disallowed]),
|
|
delete_disallowed2(Disallowed, T);
|
|
_ ->
|
|
[H|delete_disallowed2(Disallowed, T)]
|
|
end;
|
|
delete_disallowed2(_, []) ->
|
|
[].
|
|
|
|
%% @doc Keep from the list only the allowed terms.
|
|
%% Returns a sublist of Terms with only the ones which first element is
|
|
%% included in Allowed.
|
|
%% @spec (Allowed::[atom()], Terms::[term()]) -> [term()]
|
|
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),
|
|
[?WARNING_MSG("This option is not allowed, "
|
|
"and will not be accepted:~n~p", [NA])
|
|
|| NA <- NAs],
|
|
As.
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%% Support for Macro
|
|
|
|
%% @doc Replace the macros with their defined values.
|
|
%% @spec (Terms::[term()]) -> [term()]
|
|
replace_macros(Terms) ->
|
|
{TermsOthers, Macros} = split_terms_macros(Terms),
|
|
replace(TermsOthers, Macros).
|
|
|
|
%% @doc Split Terms into normal terms and macro definitions.
|
|
%% @spec (Terms) -> {Terms, Macros}
|
|
%% Terms = [term()]
|
|
%% Macros = [macro()]
|
|
split_terms_macros(Terms) ->
|
|
lists:foldl(
|
|
fun(Term, {TOs, Ms}) ->
|
|
case Term of
|
|
{define_macro, Key, Value} ->
|
|
case is_correct_macro({Key, Value}) of
|
|
true ->
|
|
{TOs, Ms++[{Key, Value}]};
|
|
false ->
|
|
exit({macro_not_properly_defined, Term})
|
|
end;
|
|
{define_macro, KeyVals} ->
|
|
case lists:all(fun is_correct_macro/1, KeyVals) of
|
|
true ->
|
|
{TOs, Ms ++ KeyVals};
|
|
false ->
|
|
exit({macros_not_properly_defined, Term})
|
|
end;
|
|
Term ->
|
|
{TOs ++ [Term], Ms}
|
|
end
|
|
end,
|
|
{[], []},
|
|
Terms).
|
|
|
|
is_correct_macro({Key, _Val}) ->
|
|
is_atom(Key) and is_all_uppercase(Key);
|
|
is_correct_macro(_) ->
|
|
false.
|
|
|
|
%% @doc Recursively replace in Terms macro usages with the defined value.
|
|
%% @spec (Terms, Macros) -> Terms
|
|
%% Terms = [term()]
|
|
%% Macros = [macro()]
|
|
replace([], _) ->
|
|
[];
|
|
replace([Term|Terms], Macros) ->
|
|
[replace_term(Term, Macros) | replace(Terms, Macros)];
|
|
replace(Term, Macros) ->
|
|
replace_term(Term, Macros).
|
|
|
|
replace_term(Key, Macros) when is_atom(Key) ->
|
|
case is_all_uppercase(Key) of
|
|
true ->
|
|
case proplists:get_value(Key, Macros) of
|
|
undefined -> exit({undefined_macro, Key});
|
|
Value -> Value
|
|
end;
|
|
false ->
|
|
Key
|
|
end;
|
|
replace_term({use_macro, Key, Value}, Macros) ->
|
|
proplists:get_value(Key, Macros, Value);
|
|
replace_term(Term, Macros) when is_list(Term) ->
|
|
replace(Term, Macros);
|
|
replace_term(Term, Macros) when is_tuple(Term) ->
|
|
List = tuple_to_list(Term),
|
|
List2 = replace(List, Macros),
|
|
list_to_tuple(List2);
|
|
replace_term(Term, _) ->
|
|
Term.
|
|
|
|
is_all_uppercase(Atom) ->
|
|
String = erlang:atom_to_list(Atom),
|
|
lists:all(fun(C) when C >= $a, C =< $z -> false;
|
|
(_) -> true
|
|
end, String).
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%% Process terms
|
|
|
|
process_term(Term, State) ->
|
|
case Term of
|
|
{host_config, HostTerms} ->
|
|
lists:foldl(
|
|
fun({Host, Terms}, AccState) ->
|
|
lists:foldl(fun(T, S) ->
|
|
process_host_term(T, Host, S, set)
|
|
end, AccState, Terms)
|
|
end, State, HostTerms);
|
|
{append_host_config, HostTerms} ->
|
|
lists:foldl(
|
|
fun({Host, Terms}, AccState) ->
|
|
lists:foldl(fun(T, S) ->
|
|
process_host_term(T, Host, S, append)
|
|
end, AccState, Terms)
|
|
end, State, HostTerms);
|
|
_ ->
|
|
process_host_term(Term, global, State, set)
|
|
end.
|
|
|
|
process_host_term(Term, Host, State, Action) ->
|
|
case Term of
|
|
{modules, Modules} when Action == set ->
|
|
set_option({modules, Host}, replace_modules(Modules), State);
|
|
{modules, Modules} when Action == append ->
|
|
append_option({modules, Host}, replace_modules(Modules), State);
|
|
{host, _} ->
|
|
State;
|
|
{hosts, _} ->
|
|
State;
|
|
{Opt, Val} when Action == set ->
|
|
set_option({rename_option(Opt), Host}, change_val(Opt, Val), State);
|
|
{Opt, Val} when Action == append ->
|
|
append_option({rename_option(Opt), Host}, change_val(Opt, Val), State);
|
|
Opt ->
|
|
?WARNING_MSG("Ignore invalid (outdated?) option ~p", [Opt]),
|
|
State
|
|
end.
|
|
|
|
rename_option(Option) when is_atom(Option) ->
|
|
case atom_to_list(Option) of
|
|
"odbc_" ++ T ->
|
|
NewOption = list_to_atom("sql_" ++ T),
|
|
?WARNING_MSG("Option '~s' is obsoleted, use '~s' instead",
|
|
[Option, NewOption]),
|
|
NewOption;
|
|
_ ->
|
|
Option
|
|
end;
|
|
rename_option(Option) ->
|
|
Option.
|
|
|
|
change_val(auth_method, Val) ->
|
|
prepare_opt_val(auth_method, Val,
|
|
fun(V) ->
|
|
L = if is_list(V) -> V;
|
|
true -> [V]
|
|
end,
|
|
lists:map(
|
|
fun(odbc) -> sql;
|
|
(internal) -> mnesia;
|
|
(A) when is_atom(A) -> A
|
|
end, L)
|
|
end, [mnesia]);
|
|
change_val(_Opt, Val) ->
|
|
Val.
|
|
|
|
set_option(Opt, Val, State) ->
|
|
State#state{opts = [#local_config{key = Opt, value = Val} |
|
|
State#state.opts]}.
|
|
|
|
append_option({Opt, Host}, Val, State) ->
|
|
GlobalVals = lists:flatmap(
|
|
fun(#local_config{key = {O, global}, value = V})
|
|
when O == Opt ->
|
|
if is_list(V) -> V;
|
|
true -> [V]
|
|
end;
|
|
(_) ->
|
|
[]
|
|
end, State#state.opts),
|
|
NewVal = if is_list(Val) -> Val ++ GlobalVals;
|
|
true -> [Val|GlobalVals]
|
|
end,
|
|
set_option({Opt, Host}, NewVal, State).
|
|
|
|
set_opts(State) ->
|
|
Opts = State#state.opts,
|
|
ets:insert(
|
|
ejabberd_options,
|
|
lists:map(
|
|
fun(#local_config{key = Key, value = Val}) ->
|
|
{Key, Val}
|
|
end, Opts)),
|
|
set_log_level().
|
|
|
|
set_log_level() ->
|
|
Level = get_option(loglevel, 4),
|
|
ejabberd_logger:set(Level).
|
|
|
|
add_global_option(Opt, Val) ->
|
|
add_option(Opt, Val).
|
|
|
|
add_local_option(Opt, Val) ->
|
|
add_option(Opt, Val).
|
|
|
|
add_option(Opt, Val) when is_atom(Opt) ->
|
|
add_option({Opt, global}, Val);
|
|
add_option({Opt, Host}, Val) ->
|
|
ets:insert(ejabberd_options, {{Opt, Host}, Val}),
|
|
ok.
|
|
|
|
-spec prepare_opt_val(any(), any(), check_fun(), any()) -> any().
|
|
|
|
prepare_opt_val(Opt, Val, F, Default) ->
|
|
Call = case F of
|
|
{Mod, Fun} ->
|
|
fun() -> Mod:Fun(Val) end;
|
|
_ ->
|
|
fun() -> F(Val) end
|
|
end,
|
|
try Call() of
|
|
Res ->
|
|
Res
|
|
catch {replace_with, NewRes} ->
|
|
NewRes;
|
|
{invalid_syntax, Error} ->
|
|
?WARNING_MSG("incorrect value '~s' of option '~s', "
|
|
"using '~s' as fallback: ~s",
|
|
[format_term(Val),
|
|
format_term(Opt),
|
|
format_term(Default),
|
|
Error]),
|
|
Default;
|
|
_:_ ->
|
|
?WARNING_MSG("incorrect value '~s' of option '~s', "
|
|
"using '~s' as fallback",
|
|
[format_term(Val),
|
|
format_term(Opt),
|
|
format_term(Default)]),
|
|
Default
|
|
end.
|
|
|
|
-type check_fun() :: fun((any()) -> any()) | {module(), atom()}.
|
|
|
|
-spec get_global_option(any(), check_fun()) -> any().
|
|
|
|
get_global_option(Opt, _) ->
|
|
get_option(Opt, undefined).
|
|
|
|
-spec get_global_option(any(), check_fun(), any()) -> any().
|
|
|
|
get_global_option(Opt, _, Default) ->
|
|
get_option(Opt, Default).
|
|
|
|
-spec get_local_option(any(), check_fun()) -> any().
|
|
|
|
get_local_option(Opt, _) ->
|
|
get_option(Opt, undefined).
|
|
|
|
-spec get_local_option(any(), check_fun(), any()) -> any().
|
|
|
|
get_local_option(Opt, _, Default) ->
|
|
get_option(Opt, Default).
|
|
|
|
-spec get_option(any()) -> any().
|
|
get_option(Opt) ->
|
|
get_option(Opt, undefined).
|
|
|
|
-spec get_option(any(), check_fun(), any()) -> any().
|
|
get_option(Opt, _, Default) ->
|
|
get_option(Opt, Default).
|
|
|
|
-spec get_option(any(), check_fun() | any()) -> any().
|
|
get_option(Opt, F) when is_function(F) ->
|
|
get_option(Opt, undefined);
|
|
get_option(Opt, Default) when is_atom(Opt) ->
|
|
get_option({Opt, global}, Default);
|
|
get_option(Opt, Default) ->
|
|
{Key, Host} = case Opt of
|
|
{O, global} when is_atom(O) -> Opt;
|
|
{O, H} when is_atom(O), is_binary(H) -> Opt;
|
|
_ ->
|
|
?WARNING_MSG("Option ~p has invalid (outdated?) "
|
|
"format. This is likely a bug", [Opt]),
|
|
{undefined, global}
|
|
end,
|
|
try ets:lookup_element(ejabberd_options, {Key, Host}, 2)
|
|
catch _:badarg when Host /= global ->
|
|
try ets:lookup_element(ejabberd_options, {Key, global}, 2)
|
|
catch _:badarg -> Default
|
|
end;
|
|
_:badarg ->
|
|
Default
|
|
end.
|
|
|
|
-spec has_option(atom() | {atom(), global | binary()}) -> any().
|
|
has_option(Opt) ->
|
|
get_option(Opt) /= undefined.
|
|
|
|
init_module_db_table(Modules) ->
|
|
%% Dirty hack for mod_pubsub
|
|
ets:insert(ejabberd_db_modules, {{mod_pubsub, mnesia}, true}),
|
|
ets:insert(ejabberd_db_modules, {{mod_pubsub, sql}, true}),
|
|
lists:foreach(
|
|
fun(M) ->
|
|
case re:split(atom_to_list(M), "_", [{return, list}]) of
|
|
[_] ->
|
|
ok;
|
|
Parts ->
|
|
[H|T] = lists:reverse(Parts),
|
|
Suffix = list_to_atom(H),
|
|
BareMod = list_to_atom(string:join(lists:reverse(T), "_")),
|
|
case is_behaviour(BareMod, M) of
|
|
true ->
|
|
ets:insert(ejabberd_db_modules,
|
|
{{BareMod, Suffix}, true});
|
|
false ->
|
|
ok
|
|
end
|
|
end
|
|
end, Modules).
|
|
|
|
is_behaviour(Behav, Mod) ->
|
|
try Mod:module_info(attributes) of
|
|
[] ->
|
|
%% Stripped module?
|
|
true;
|
|
Attrs ->
|
|
lists:any(
|
|
fun({behaviour, L}) -> lists:member(Behav, L);
|
|
({behavior, L}) -> lists:member(Behav, L);
|
|
(_) -> false
|
|
end, Attrs)
|
|
catch _:_ ->
|
|
true
|
|
end.
|
|
|
|
-spec v_db(module(), atom()) -> atom().
|
|
|
|
v_db(Mod, internal) -> v_db(Mod, mnesia);
|
|
v_db(Mod, odbc) -> v_db(Mod, sql);
|
|
v_db(Mod, Type) ->
|
|
case ets:member(ejabberd_db_modules, {Mod, Type}) of
|
|
true -> Type;
|
|
false -> erlang:error(badarg)
|
|
end.
|
|
|
|
-spec v_dbs(module()) -> [atom()].
|
|
|
|
v_dbs(Mod) ->
|
|
ets:select(
|
|
ejabberd_db_modules,
|
|
ets:fun2ms(
|
|
fun({{M, Type}, _}) when M == Mod ->
|
|
Type
|
|
end)).
|
|
|
|
-spec v_dbs_mods(module()) -> [module()].
|
|
|
|
v_dbs_mods(Mod) ->
|
|
lists:map(fun(M) ->
|
|
binary_to_atom(<<(atom_to_binary(Mod, utf8))/binary, "_",
|
|
(atom_to_binary(M, utf8))/binary>>, utf8)
|
|
end, v_dbs(Mod)).
|
|
|
|
-spec default_db(module()) -> atom().
|
|
default_db(Module) ->
|
|
default_db(global, Module).
|
|
|
|
-spec default_db(binary() | global, module()) -> atom().
|
|
default_db(Host, Module) ->
|
|
default_db(default_db, Host, Module).
|
|
|
|
-spec default_ram_db(module()) -> atom().
|
|
default_ram_db(Module) ->
|
|
default_ram_db(global, Module).
|
|
|
|
-spec default_ram_db(binary() | global, module()) -> atom().
|
|
default_ram_db(Host, Module) ->
|
|
default_db(default_ram_db, Host, Module).
|
|
|
|
-spec default_db(default_db | default_ram_db, binary() | global, module()) -> atom().
|
|
default_db(Opt, Host, Module) ->
|
|
case get_option({Opt, Host}) of
|
|
undefined ->
|
|
mnesia;
|
|
DBType ->
|
|
try
|
|
v_db(Module, DBType)
|
|
catch error:badarg ->
|
|
?WARNING_MSG("Module '~s' doesn't support database '~s' "
|
|
"defined in option '~s', using "
|
|
"'mnesia' as fallback", [Module, DBType, Opt]),
|
|
mnesia
|
|
end
|
|
end.
|
|
|
|
get_modules() ->
|
|
{ok, Mods} = application:get_key(ejabberd, modules),
|
|
ExtMods = [Name || {Name, _Details} <- ext_mod:installed()],
|
|
case application:get_env(ejabberd, external_beams) of
|
|
{ok, Path} ->
|
|
case lists:member(Path, code:get_path()) of
|
|
true -> ok;
|
|
false -> code:add_patha(Path)
|
|
end,
|
|
Beams = filelib:wildcard(filename:join(Path, "*\.beam")),
|
|
CustMods = [list_to_atom(filename:rootname(filename:basename(Beam)))
|
|
|| Beam <- Beams],
|
|
CustMods ++ ExtMods ++ Mods;
|
|
_ ->
|
|
ExtMods ++ Mods
|
|
end.
|
|
|
|
get_modules_with_options(Modules) ->
|
|
lists:foldl(
|
|
fun(Mod, D) ->
|
|
case is_behaviour(?MODULE, Mod) orelse Mod == ?MODULE of
|
|
true ->
|
|
try Mod:opt_type('') of
|
|
Opts when is_list(Opts) ->
|
|
lists:foldl(
|
|
fun(Opt, Acc) ->
|
|
dict:append(Opt, Mod, Acc)
|
|
end, D, Opts)
|
|
catch _:undef ->
|
|
D
|
|
end;
|
|
false ->
|
|
D
|
|
end
|
|
end, dict:new(), Modules).
|
|
|
|
-spec validate_opts(#state{}, dict:dict()) -> {ok, #state{}} | {error, bad_option()}.
|
|
validate_opts(#state{opts = Opts} = State, ModOpts) ->
|
|
try
|
|
NewOpts = lists:map(
|
|
fun(#local_config{key = {Opt, _Host}, value = Val} = In) ->
|
|
case dict:find(Opt, ModOpts) of
|
|
{ok, [Mod|_]} ->
|
|
VFun = Mod:opt_type(Opt),
|
|
try VFun(Val) of
|
|
NewVal ->
|
|
In#local_config{value = NewVal}
|
|
catch {invalid_syntax, Error} ->
|
|
?ERROR_MSG("Invalid value '~p' for "
|
|
"option '~s': ~s",
|
|
[Val, Opt, Error]),
|
|
erlang:error(invalid_option);
|
|
_:_ ->
|
|
?ERROR_MSG("Invalid value '~p' for "
|
|
"option '~s'",
|
|
[Val, Opt]),
|
|
erlang:error(invalid_option)
|
|
end;
|
|
_ ->
|
|
?ERROR_MSG("Unknown option '~s'", [Opt]),
|
|
erlang:error(unknown_option)
|
|
end
|
|
end, Opts),
|
|
{ok, State#state{opts = NewOpts}}
|
|
catch _:invalid_option ->
|
|
{error, invalid_option};
|
|
_:unknown_option ->
|
|
{error, unknown_option}
|
|
end.
|
|
|
|
%% @spec (Path::string()) -> true | false
|
|
is_file_readable(Path) ->
|
|
case file:read_file_info(Path) of
|
|
{ok, FileInfo} ->
|
|
case {FileInfo#file_info.type, FileInfo#file_info.access} of
|
|
{regular, read} -> true;
|
|
{regular, read_write} -> true;
|
|
_ -> false
|
|
end;
|
|
{error, _Reason} ->
|
|
false
|
|
end.
|
|
|
|
get_version() ->
|
|
case application:get_key(ejabberd, vsn) of
|
|
undefined -> "";
|
|
{ok, Vsn} -> list_to_binary(Vsn)
|
|
end.
|
|
|
|
-spec get_myhosts() -> [binary()].
|
|
|
|
get_myhosts() ->
|
|
get_option(hosts).
|
|
|
|
-spec get_mylang() -> binary().
|
|
|
|
get_mylang() ->
|
|
get_lang(global).
|
|
|
|
-spec get_lang(global | binary()) -> binary().
|
|
get_lang(Host) ->
|
|
get_option({language, Host}, <<"en">>).
|
|
|
|
replace_module(mod_announce_odbc) -> {mod_announce, sql};
|
|
replace_module(mod_blocking_odbc) -> {mod_blocking, sql};
|
|
replace_module(mod_caps_odbc) -> {mod_caps, sql};
|
|
replace_module(mod_irc_odbc) -> {mod_irc, sql};
|
|
replace_module(mod_last_odbc) -> {mod_last, sql};
|
|
replace_module(mod_muc_odbc) -> {mod_muc, sql};
|
|
replace_module(mod_offline_odbc) -> {mod_offline, sql};
|
|
replace_module(mod_privacy_odbc) -> {mod_privacy, sql};
|
|
replace_module(mod_private_odbc) -> {mod_private, sql};
|
|
replace_module(mod_roster_odbc) -> {mod_roster, sql};
|
|
replace_module(mod_shared_roster_odbc) -> {mod_shared_roster, sql};
|
|
replace_module(mod_vcard_odbc) -> {mod_vcard, sql};
|
|
replace_module(mod_vcard_ldap) -> {mod_vcard, ldap};
|
|
replace_module(mod_vcard_xupdate_odbc) -> mod_vcard_xupdate;
|
|
replace_module(mod_pubsub_odbc) -> {mod_pubsub, sql};
|
|
replace_module(mod_http_bind) -> mod_bosh;
|
|
replace_module(Module) ->
|
|
case is_elixir_module(Module) of
|
|
true -> expand_elixir_module(Module);
|
|
false -> Module
|
|
end.
|
|
|
|
replace_modules(Modules) ->
|
|
lists:map(
|
|
fun({Module, Opts}) ->
|
|
case replace_module(Module) of
|
|
{NewModule, DBType} ->
|
|
emit_deprecation_warning(Module, NewModule, DBType),
|
|
NewOpts = [{db_type, DBType} |
|
|
lists:keydelete(db_type, 1, Opts)],
|
|
{NewModule, transform_module_options(Module, NewOpts)};
|
|
NewModule ->
|
|
if Module /= NewModule ->
|
|
emit_deprecation_warning(Module, NewModule);
|
|
true ->
|
|
ok
|
|
end,
|
|
{NewModule, transform_module_options(Module, Opts)}
|
|
end
|
|
end, Modules).
|
|
|
|
%% Elixir module naming
|
|
%% ====================
|
|
|
|
-ifdef(ELIXIR_ENABLED).
|
|
is_elixir_enabled() ->
|
|
true.
|
|
-else.
|
|
is_elixir_enabled() ->
|
|
false.
|
|
-endif.
|
|
|
|
is_using_elixir_config() ->
|
|
case is_elixir_enabled() of
|
|
true ->
|
|
Config = get_ejabberd_config_path(),
|
|
'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config);
|
|
false ->
|
|
false
|
|
end.
|
|
|
|
%% If module name start with uppercase letter, this is an Elixir module:
|
|
is_elixir_module(Module) ->
|
|
case atom_to_list(Module) of
|
|
[H|_] when H >= 65, H =< 90 -> true;
|
|
_ ->false
|
|
end.
|
|
|
|
%% We assume we know this is an elixir module
|
|
expand_elixir_module(Module) ->
|
|
case atom_to_list(Module) of
|
|
%% Module name already specified as an Elixir from Erlang module name
|
|
"Elixir." ++ _ -> Module;
|
|
%% if start with uppercase letter, this is an Elixir module: Append 'Elixir.' to module name.
|
|
ModuleString ->
|
|
list_to_atom("Elixir." ++ ModuleString)
|
|
end.
|
|
|
|
strings_to_binary([]) ->
|
|
[];
|
|
strings_to_binary(L) when is_list(L) ->
|
|
case is_string(L) of
|
|
true ->
|
|
list_to_binary(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) ->
|
|
{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([]) ->
|
|
[];
|
|
strings_to_binary1(T) ->
|
|
T.
|
|
|
|
is_string([C|T]) when (C >= 0) and (C =< 255) ->
|
|
is_string(T);
|
|
is_string([]) ->
|
|
true;
|
|
is_string(_) ->
|
|
false.
|
|
|
|
binary_to_strings(B) when is_binary(B) ->
|
|
binary_to_list(B);
|
|
binary_to_strings([H|T]) ->
|
|
[binary_to_strings(H)|binary_to_strings(T)];
|
|
binary_to_strings(T) when is_tuple(T) ->
|
|
list_to_tuple(binary_to_strings(tuple_to_list(T)));
|
|
binary_to_strings(T) ->
|
|
T.
|
|
|
|
format_term(Bin) when is_binary(Bin) ->
|
|
io_lib:format("\"~s\"", [Bin]);
|
|
format_term(S) when is_list(S), S /= [] ->
|
|
case lists:all(fun(C) -> (C>=0) and (C=<255) end, S) of
|
|
true ->
|
|
io_lib:format("\"~s\"", [S]);
|
|
false ->
|
|
io_lib:format("~p", [binary_to_strings(S)])
|
|
end;
|
|
format_term(T) ->
|
|
io_lib:format("~p", [binary_to_strings(T)]).
|
|
|
|
transform_terms(Terms) ->
|
|
%% We could check all ejabberd beams, but this
|
|
%% slows down start-up procedure :(
|
|
Mods = [mod_register,
|
|
ejabberd_s2s,
|
|
ejabberd_listener,
|
|
ejabberd_sql_sup,
|
|
shaper,
|
|
ejabberd_s2s_out,
|
|
acl,
|
|
ejabberd_config],
|
|
collect_options(transform_terms(Mods, Terms)).
|
|
|
|
transform_terms([Mod|Mods], Terms) ->
|
|
case catch Mod:transform_options(Terms) of
|
|
{'EXIT', _} = Err ->
|
|
?ERROR_MSG("Failed to transform terms by ~p: ~p", [Mod, Err]),
|
|
transform_terms(Mods, Terms);
|
|
NewTerms ->
|
|
transform_terms(Mods, NewTerms)
|
|
end;
|
|
transform_terms([], NewTerms) ->
|
|
NewTerms.
|
|
|
|
transform_module_options(Module, Opts) ->
|
|
Opts1 = gen_iq_handler:transform_module_options(Opts),
|
|
try
|
|
Module:transform_module_options(Opts1)
|
|
catch error:undef ->
|
|
Opts1
|
|
end.
|
|
|
|
compact(Cfg) ->
|
|
Opts = [{K, V} || #local_config{key = K, value = V} <- Cfg],
|
|
{GOpts, HOpts} = split_by_hosts(Opts),
|
|
[#local_config{key = {O, global}, value = V} || {O, V} <- GOpts] ++
|
|
lists:flatmap(
|
|
fun({Host, OptVal}) ->
|
|
case lists:member(OptVal, GOpts) of
|
|
true ->
|
|
[];
|
|
false ->
|
|
[#local_config{key = {Opt, Host}, value = Val}
|
|
|| {Opt, Val} <- OptVal]
|
|
end
|
|
end, lists:flatten(HOpts)).
|
|
|
|
split_by_hosts(Opts) ->
|
|
Opts1 = orddict:to_list(
|
|
lists:foldl(
|
|
fun({{Opt, Host}, Val}, D) ->
|
|
orddict:append(Host, {Opt, Val}, D)
|
|
end, orddict:new(), Opts)),
|
|
case lists:keytake(global, 1, Opts1) of
|
|
{value, {global, GlobalOpts}, HostOpts} ->
|
|
{GlobalOpts, HostOpts};
|
|
_ ->
|
|
{[], Opts1}
|
|
end.
|
|
|
|
collect_options(Opts) ->
|
|
{D, InvalidOpts} =
|
|
lists:foldl(
|
|
fun({K, V}, {D, Os}) when is_list(V) ->
|
|
{orddict:append_list(K, V, D), Os};
|
|
({K, V}, {D, Os}) ->
|
|
{orddict:store(K, V, D), Os};
|
|
(Opt, {D, Os}) ->
|
|
{D, [Opt|Os]}
|
|
end, {orddict:new(), []}, Opts),
|
|
InvalidOpts ++ orddict:to_list(D).
|
|
|
|
transform_options(Opts) ->
|
|
Opts1 = lists:foldl(fun transform_options/2, [], Opts),
|
|
{HOpts, Opts2} = lists:mapfoldl(
|
|
fun({host_config, O}, Os) ->
|
|
{[O], Os};
|
|
(O, Os) ->
|
|
{[], [O|Os]}
|
|
end, [], Opts1),
|
|
{AHOpts, Opts3} = lists:mapfoldl(
|
|
fun({append_host_config, O}, Os) ->
|
|
{[O], Os};
|
|
(O, Os) ->
|
|
{[], [O|Os]}
|
|
end, [], Opts2),
|
|
HOpts1 = case collect_options(lists:flatten(HOpts)) of
|
|
[] ->
|
|
[];
|
|
HOs ->
|
|
[{host_config,
|
|
[{H, transform_terms(O)} || {H, O} <- HOs]}]
|
|
end,
|
|
AHOpts1 = case collect_options(lists:flatten(AHOpts)) of
|
|
[] ->
|
|
[];
|
|
AHOs ->
|
|
[{append_host_config,
|
|
[{H, transform_terms(O)} || {H, O} <- AHOs]}]
|
|
end,
|
|
HOpts1 ++ AHOpts1 ++ Opts3.
|
|
|
|
transform_options({domain_certfile, Domain, CertFile}, Opts) ->
|
|
?WARNING_MSG("Option 'domain_certfile' now should be defined "
|
|
"per virtual host or globally. The old format is "
|
|
"still supported but it is better to fix your config", []),
|
|
[{host_config, [{Domain, [{domain_certfile, CertFile}]}]}|Opts];
|
|
transform_options(Opt, Opts) when Opt == override_global;
|
|
Opt == override_local;
|
|
Opt == override_acls ->
|
|
?WARNING_MSG("Ignoring '~s' option which has no effect anymore", [Opt]),
|
|
Opts;
|
|
transform_options({node_start, {_, _, _} = Now}, Opts) ->
|
|
?WARNING_MSG("Old 'node_start' format detected. This is still supported "
|
|
"but it is better to fix your config.", []),
|
|
[{node_start, now_to_seconds(Now)}|Opts];
|
|
transform_options({host_config, Host, HOpts}, Opts) ->
|
|
{AddOpts, HOpts1} =
|
|
lists:mapfoldl(
|
|
fun({{add, Opt}, Val}, Os) ->
|
|
?WARNING_MSG("Option 'add' is deprecated. "
|
|
"The option is still supported "
|
|
"but it is better to fix your config: "
|
|
"use 'append_host_config' instead.", []),
|
|
{[{Opt, Val}], Os};
|
|
(O, Os) ->
|
|
{[], [O|Os]}
|
|
end, [], HOpts),
|
|
[{append_host_config, [{Host, lists:flatten(AddOpts)}]},
|
|
{host_config, [{Host, HOpts1}]}|Opts];
|
|
transform_options({define_macro, Macro, Val}, Opts) ->
|
|
[{define_macro, [{Macro, Val}]}|Opts];
|
|
transform_options({include_config_file, _} = Opt, Opts) ->
|
|
[{include_config_file, [transform_include_option(Opt)]} | Opts];
|
|
transform_options({include_config_file, _, _} = Opt, Opts) ->
|
|
[{include_config_file, [transform_include_option(Opt)]} | Opts];
|
|
transform_options(Opt, Opts) ->
|
|
[Opt|Opts].
|
|
|
|
emit_deprecation_warning(Module, NewModule, DBType) ->
|
|
?WARNING_MSG("Module ~s is deprecated, use ~s with 'db_type: ~s'"
|
|
" instead", [Module, NewModule, DBType]).
|
|
|
|
emit_deprecation_warning(Module, NewModule) ->
|
|
case is_elixir_module(NewModule) of
|
|
%% Do not emit deprecation warning for Elixir
|
|
true -> ok;
|
|
false ->
|
|
?WARNING_MSG("Module ~s is deprecated, use ~s instead",
|
|
[Module, NewModule])
|
|
end.
|
|
|
|
-spec now_to_seconds(erlang:timestamp()) -> non_neg_integer().
|
|
now_to_seconds({MegaSecs, Secs, _MicroSecs}) ->
|
|
MegaSecs * 1000000 + Secs.
|
|
|
|
-spec opt_type(hide_sensitive_log_data) -> fun((boolean()) -> boolean());
|
|
(hosts) -> fun(([binary()]) -> [binary()]);
|
|
(language) -> fun((binary()) -> binary());
|
|
(max_fsm_queue) -> fun((pos_integer()) -> pos_integer());
|
|
(default_db) -> fun((atom()) -> atom());
|
|
(default_ram_db) -> fun((atom()) -> atom());
|
|
(loglevel) -> fun((0..5) -> 0..5);
|
|
(queue_dir) -> fun((binary()) -> binary());
|
|
(queue_type) -> fun((ram | file) -> ram | file);
|
|
(use_cache) -> fun((boolean()) -> boolean());
|
|
(cache_size) -> fun((timeout()) -> timeout());
|
|
(cache_missed) -> fun((boolean()) -> boolean());
|
|
(cache_life_time) -> fun((timeout()) -> timeout());
|
|
(shared_key) -> fun((binary()) -> binary());
|
|
(node_start) -> fun((non_neg_integer()) -> non_neg_integer());
|
|
(atom()) -> [atom()].
|
|
opt_type(hide_sensitive_log_data) ->
|
|
fun (H) when is_boolean(H) -> H end;
|
|
opt_type(hosts) ->
|
|
fun(L) ->
|
|
[iolist_to_binary(H) || H <- L]
|
|
end;
|
|
opt_type(language) ->
|
|
fun iolist_to_binary/1;
|
|
opt_type(max_fsm_queue) ->
|
|
fun (I) when is_integer(I), I > 0 -> I end;
|
|
opt_type(default_db) ->
|
|
fun(T) when is_atom(T) -> T end;
|
|
opt_type(default_ram_db) ->
|
|
fun(T) when is_atom(T) -> T end;
|
|
opt_type(loglevel) ->
|
|
fun (P) when P >= 0, P =< 5 -> P end;
|
|
opt_type(queue_dir) ->
|
|
fun iolist_to_binary/1;
|
|
opt_type(queue_type) ->
|
|
fun(ram) -> ram; (file) -> file end;
|
|
opt_type(use_cache) ->
|
|
fun(B) when is_boolean(B) -> B end;
|
|
opt_type(cache_size) ->
|
|
fun(I) when is_integer(I), I>0 -> I;
|
|
(infinity) -> infinity;
|
|
(unlimited) -> infinity
|
|
end;
|
|
opt_type(cache_missed) ->
|
|
fun(B) when is_boolean(B) -> B end;
|
|
opt_type(cache_life_time) ->
|
|
fun(I) when is_integer(I), I>0 -> I;
|
|
(infinity) -> infinity;
|
|
(unlimited) -> infinity
|
|
end;
|
|
opt_type(negotiation_timeout) ->
|
|
fun(T) when T > 0 -> T end;
|
|
opt_type(shared_key) ->
|
|
fun iolist_to_binary/1;
|
|
opt_type(node_start) ->
|
|
fun(I) when is_integer(I), I>=0 -> I end;
|
|
opt_type(validate_stream) ->
|
|
fun(B) when is_boolean(B) -> B end;
|
|
opt_type(_) ->
|
|
[hide_sensitive_log_data, hosts, language, max_fsm_queue,
|
|
default_db, default_ram_db, queue_type, queue_dir, loglevel,
|
|
use_cache, cache_size, cache_missed, cache_life_time,
|
|
shared_key, node_start, validate_stream, negotiation_timeout].
|
|
|
|
-spec may_hide_data(any()) -> any().
|
|
may_hide_data(Data) ->
|
|
case get_option(hide_sensitive_log_data, false) of
|
|
false ->
|
|
Data;
|
|
true ->
|
|
"hidden_by_ejabberd"
|
|
end.
|
|
|
|
-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 ->
|
|
[{max_queue, I}];
|
|
false ->
|
|
case get_option(max_fsm_queue) of
|
|
undefined -> [];
|
|
N -> [{max_queue, N}]
|
|
end
|
|
end.
|
|
|
|
-spec queue_dir() -> binary() | undefined.
|
|
queue_dir() ->
|
|
get_option(queue_dir).
|
|
|
|
-spec default_queue_type(binary()) -> ram | file.
|
|
default_queue_type(Host) ->
|
|
get_option({queue_type, Host}, ram).
|
|
|
|
-spec use_cache(binary() | global) -> boolean().
|
|
use_cache(Host) ->
|
|
get_option({use_cache, Host}, true).
|
|
|
|
-spec cache_size(binary() | global) -> pos_integer() | infinity.
|
|
cache_size(Host) ->
|
|
get_option({cache_size, Host}, 1000).
|
|
|
|
-spec cache_missed(binary() | global) -> boolean().
|
|
cache_missed(Host) ->
|
|
get_option({cache_missed, Host}, true).
|
|
|
|
-spec cache_life_time(binary() | global) -> pos_integer() | infinity.
|
|
%% NOTE: the integer value returned is in *seconds*
|
|
cache_life_time(Host) ->
|
|
get_option({cache_life_time, Host}, 3600).
|
|
|
|
-spec codec_options(binary() | global) -> [xmpp:decode_option()].
|
|
codec_options(Host) ->
|
|
case get_option({validate_stream, Host}, false) of
|
|
true -> [];
|
|
false -> [ignore_els]
|
|
end.
|
|
|
|
-spec negotiation_timeout() -> pos_integer().
|
|
negotiation_timeout() ->
|
|
timer:seconds(get_option(negotiation_timeout, 30)).
|