%%%---------------------------------------------------------------------- %%% File : ejabberd_config.erl %%% Author : Alexey Shchepin %%% Purpose : Load config file %%% Created : 14 Dec 2002 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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). %% API -export([get_option/1]). -export([load/0, reload/0, format_error/1, path/0]). -export([env_binary_to_list/2]). -export([get_myname/0, get_uri/0, get_copyright/0]). -export([get_shared_key/0, get_node_start/0]). -export([fsm_limit_opts/1]). -export([codec_options/0]). -export([version/0]). -export([default_db/2, default_db/3, default_ram_db/2, default_ram_db/3]). -export([beams/1, validators/1, globals/0, may_hide_data/1]). -export([dump/0, dump/1, convert_to_yaml/1, convert_to_yaml/2]). -export([callback_modules/1]). %% Deprecated functions -export([get_option/2, set_option/2]). -export([get_version/0, get_myhosts/0]). -export([get_mylang/0, get_lang/1]). -deprecated([{get_option, 2}, {set_option, 2}, {get_version, 0}, {get_myhosts, 0}, {get_mylang, 0}, {get_lang, 1}]). -include("logger.hrl"). -include("ejabberd_stacktrace.hrl"). -type option() :: atom() | {atom(), global | binary()}. -type error_reason() :: {merge_conflict, atom(), binary()} | {old_config, file:filename_all(), term()} | {write_file, file:filename_all(), term()} | {exception, term(), term(), term()}. -type error_return() :: {error, econf:error_reason(), term()} | {error, error_reason()}. -type host_config() :: #{{atom(), binary() | global} => term()}. -callback opt_type(atom()) -> econf:validator(). -callback options() -> [atom() | {atom(), term()}]. -callback globals() -> [atom()]. -callback doc() -> any(). -optional_callbacks([globals/0]). %%%=================================================================== %%% API %%%=================================================================== -spec load() -> ok | error_return(). load() -> load(path()). -spec load(file:filename_all()) -> ok | error_return(). load(Path) -> ConfigFile = unicode:characters_to_binary(Path), UnixTime = erlang:monotonic_time(second), ?INFO_MSG("Loading configuration from ~ts", [ConfigFile]), _ = ets:new(ejabberd_options, [named_table, public, {read_concurrency, true}]), case load_file(ConfigFile) of ok -> set_shared_key(), set_node_start(UnixTime), ?INFO_MSG("Configuration loaded successfully", []); Err -> Err end. -spec reload() -> ok | error_return(). reload() -> ejabberd_systemd:reloading(), ConfigFile = path(), ?INFO_MSG("Reloading configuration from ~ts", [ConfigFile]), OldHosts = get_myhosts(), Res = case load_file(ConfigFile) of ok -> NewHosts = get_myhosts(), AddHosts = NewHosts -- OldHosts, DelHosts = OldHosts -- NewHosts, lists:foreach( fun(Host) -> ejabberd_hooks:run(host_up, [Host]) end, AddHosts), lists:foreach( fun(Host) -> ejabberd_hooks:run(host_down, [Host]) end, DelHosts), ejabberd_hooks:run(config_reloaded, []), delete_host_options(DelHosts), ?INFO_MSG("Configuration reloaded successfully", []); Err -> ?ERROR_MSG("Configuration reload aborted: ~ts", [format_error(Err)]), Err end, 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), case Output of stdout -> io:format("~ts~n", [Data]); FileName -> try ok = filelib:ensure_dir(FileName), ok = file:write_file(FileName, Data) 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 end. -spec get_option(option()) -> term(). get_option(Opt) when is_atom(Opt) -> get_option({Opt, global}); get_option({O, Host} = Opt) -> Tab = case get_tmp_config() of undefined -> ejabberd_options; T -> T end, 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'. " "This is a bug, please report it with the following " "stacktrace included:~n** ~ts", [O, Host, misc:format_exception(2, error, badarg, StackTrace)]), Val end. -spec set_option(option(), term()) -> ok. set_option(Opt, Val) when is_atom(Opt) -> set_option({Opt, global}, Val); set_option(Opt, Val) -> Tab = case get_tmp_config() of undefined -> ejabberd_options; T -> T end, 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() -> <<"http://www.process-one.net/en/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 -> [{max_queue, I}]; false -> case get_option(max_fsm_queue) of undefined -> []; N -> [{max_queue, N}] end end. -spec codec_options() -> [xmpp:decode_option()]. codec_options() -> case get_option(validate_stream) of true -> []; 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. -spec version() -> binary(). version() -> case application:get_env(ejabberd, custom_vsn) of {ok, Vsn0} when is_list(Vsn0) -> list_to_binary(Vsn0); {ok, Vsn1} when is_binary(Vsn1) -> Vsn1; _ -> case application:get_key(ejabberd, vsn) of undefined -> <<"">>; {ok, Vsn} -> list_to_binary(Vsn) end end. -spec default_db(binary() | global, module()) -> atom(). default_db(Host, Module) -> default_db(default_db, Host, Module, mnesia). -spec default_db(binary() | global, module(), atom()) -> atom(). default_db(Host, Module, Default) -> default_db(default_db, Host, Module, Default). -spec default_ram_db(binary() | global, module()) -> atom(). default_ram_db(Host, Module) -> default_db(default_ram_db, Host, Module, mnesia). -spec default_ram_db(binary() | global, module(), atom()) -> atom(). default_ram_db(Host, Module, Default) -> default_db(default_ram_db, Host, Module, Default). -spec default_db(default_db | default_ram_db, binary() | global, module(), atom()) -> atom(). default_db(Opt, Host, Mod, Default) -> Type = get_option({Opt, Host}), DBMod = list_to_atom(atom_to_list(Mod) ++ "_" ++ atom_to_list(Type)), case code:ensure_loaded(DBMod) of {module, _} -> Type; {error, _} -> ?WARNING_MSG("Module ~ts doesn't support database '~ts' " "defined in option '~ts', using " "'~ts' as fallback", [Mod, Type, Opt, 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()], lists:foreach( fun(ExtMod) -> ExtModPath = ext_mod:module_ebin_dir(ExtMod), case lists:member(ExtModPath, code:get_path()) of true -> ok; false -> code:add_patha(ExtModPath) end end, ExtMods), 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; _ -> ExtMods end; beams(all) -> beams(local) ++ beams(external). -spec may_hide_data(term()) -> term(). may_hide_data(Data) -> case get_option(hide_sensitive_log_data) of false -> 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. -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. -spec validators([atom()]) -> {econf:validators(), [atom()]}. validators(Disallowed) -> Modules = callback_modules(all), Validators = lists:foldl( fun(M, Vs) -> maps:merge(Vs, validators(M, Disallowed)) end, #{}, Modules), Required = lists:flatmap( fun(M) -> [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) -> case read_erlang_file(File, []) of {ok, Y} -> dump(Y, Output); Err -> Err end. -spec format_error(error_return()) -> string(). format_error({error, Reason, Ctx}) -> econf:format_error(Reason, Ctx); format_error({error, {merge_conflict, Opt, Host}}) -> lists:flatten( io_lib:format( "Cannot merge value of option '~ts' defined in append_host_config " "for virtual host ~ts: only options of type list or map are allowed " "in append_host_config. Hint: specify the option in host_config", [Opt, Host])); format_error({error, {old_config, Path, Reason}}) -> lists:flatten( io_lib:format( "Failed to read configuration from '~ts': ~ts~ts", [Path, case Reason of {_, _, _} -> "at line "; _ -> "" end, file:format_error(Reason)])); format_error({error, {write_file, Path, Reason}}) -> lists:flatten( io_lib:format( "Failed to write to '~ts': ~ts", [Path, file:format_error(Reason)])); format_error({error, {exception, Class, Reason, St}}) -> lists:flatten( io_lib:format( "Exception occurred during configuration processing. " "This is most likely due to faulty/incompatible validator in " "third-party code. If you are not running any third-party " "code, please report the bug with ejabberd configuration " "file attached and the following stacktrace included:~n** ~ts", [misc:format_exception(2, Class, Reason, St)])). %%%=================================================================== %%% Internal functions %%%=================================================================== -spec path() -> binary(). path() -> unicode:characters_to_binary( case get_env_config() of {ok, Path} -> Path; undefined -> case os:getenv("EJABBERD_CONFIG_PATH") of false -> "ejabberd.yml"; 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. -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 undefined -> ok; T -> erase(ejabberd_options), ets:delete(T), ok end. -spec callback_modules(local | external | all) -> [module()]. callback_modules(local) -> [ejabberd_options]; callback_modules(external) -> lists:filter( fun(M) -> case code:ensure_loaded(M) of {module, _} -> erlang:function_exported(M, options, 0) andalso erlang:function_exported(M, opt_type, 1); {error, _} -> false end end, beams(external)); callback_modules(all) -> callback_modules(local) ++ callback_modules(external). -spec validators(module(), [atom()]) -> econf:validators(). validators(Mod, Disallowed) -> maps:from_list( lists:filtermap( fun(O) -> case lists:member(O, Disallowed) of true -> false; false -> {true, try {O, Mod:opt_type(O)} catch _:_ -> {O, ejabberd_options:opt_type(O)} end} end 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 Ex when Ex == <<".yml">> orelse Ex == <<".yaml">> -> Files = case proplists:get_bool(include_modules_configs, Opts2) of true -> ext_mod:modules_configs(); false -> [] end, lists:foreach( fun(F) -> ?INFO_MSG("Loading third-party configuration from ~ts", [F]) end, Files), read_yaml_files([File|Files], lists:flatten(Opts1)); _ -> read_erlang_file(File, lists:flatten(Opts1)) end, case Ret of {ok, Y} -> validate(Y); Err -> Err end. read_yaml_files(Files, Opts) -> ParseOpts = [plain_as_atom | lists:flatten(Opts)], lists:foldl( fun(File, {ok, Y1}) -> case econf:parse(File, #{'_' => econf:any()}, ParseOpts) of {ok, Y2} -> {ok, Y1 ++ Y2}; Err -> Err end; (_, Err) -> Err end, {ok, []}, Files). read_erlang_file(File, _) -> case ejabberd_old_config:read_file(File) of {ok, Y} -> econf:replace_macros(Y); Err -> Err end. -spec validate(term()) -> {ok, [{atom(), term()}]} | error_return(). validate(Y1) -> case pre_validate(Y1) of {ok, Y2} -> set_loglevel(proplists:get_value(loglevel, Y2, info)), case ejabberd_config_transformer:map_reduce(Y2) of {ok, Y3} -> Hosts = proplists:get_value(hosts, Y3), Version = proplists:get_value(version, Y3, version()), create_tmp_config(), set_option(hosts, Hosts), set_option(host, hd(Hosts)), set_option(version, Version), set_option(yaml_config, Y3), {Validators, Required} = validators([]), Validator = econf:options(Validators, [{required, Required}, unique]), econf:validate(Validator, Y3); Err -> Err end; Err -> 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), loglevel => ejabberd_options:opt_type(loglevel), version => ejabberd_options:opt_type(version), '_' => econf:any()}, [{required, [hosts]}]), fun econf:group_dups/1), Y1). -spec load_file(binary()) -> ok | error_return(). load_file(File) -> try case read_file(File) of {ok, Terms} -> case set_host_config(Terms) of {ok, Map} -> T = get_tmp_config(), Hosts = get_myhosts(), apply_defaults(T, Hosts, Map), case validate_modules(Hosts) of {ok, ModOpts} -> ets:insert(T, ModOpts), set_option(host, hd(Hosts)), commit(), set_fqdn(); Err -> abort(Err) end; Err -> abort(Err) end; Err -> abort(Err) end catch ?EX_RULE(Class, Reason, St) -> {error, {exception, Class, Reason, ?EX_STACK(St)}} end. -spec commit() -> ok. commit() -> T = get_tmp_config(), NewOpts = ets:tab2list(T), 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 -> ok end, Err. -spec set_host_config([{atom(), term()}]) -> {ok, host_config()} | error_return(). set_host_config(Opts) -> Map1 = lists:foldl( fun({Opt, Val}, M) when Opt /= host_config, Opt /= append_host_config -> maps:put({Opt, global}, Val, M); (_, M) -> M end, #{}, Opts), HostOpts = proplists:get_value(host_config, Opts, []), AppendHostOpts = proplists:get_value(append_host_config, Opts, []), Map2 = lists:foldl( fun({Host, Opts1}, M1) -> lists:foldl( fun({Opt, Val}, M2) -> maps:put({Opt, Host}, Val, M2) end, M1, Opts1) end, Map1, HostOpts), Map3 = lists:foldl( fun(_, {error, _} = Err) -> Err; ({Host, Opts1}, M1) -> lists:foldl( fun(_, {error, _} = Err) -> Err; ({Opt, L1}, M2) when is_list(L1) -> 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), case Map3 of {error, _} -> Map3; _ -> {ok, Map3} end. -spec apply_defaults(ets:tid(), [binary()], host_config()) -> ok. apply_defaults(Tab, Hosts, Map) -> Defaults1 = defaults(), apply_defaults(Tab, global, Map, Defaults1), {_, Defaults2} = proplists:split(Defaults1, globals()), lists:foreach( fun(Host) -> set_option(host, Host), apply_defaults(Tab, Host, Map, Defaults2) end, Hosts). -spec apply_defaults(ets:tid(), global | binary(), host_config(), [atom() | {atom(), term()}]) -> ok. apply_defaults(Tab, Host, Map, Defaults) -> lists:foreach( fun({Opt, Default}) -> try maps:get({Opt, Host}, Map) of Val -> ets:insert(Tab, {{Opt, Host}, Val}) 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, _} -> Default1 = compute_default(Default, Host), ets:insert(Tab, {{Opt, Host}, Default1}) end end; (Opt) when Host == global -> Val = maps:get({Opt, Host}, Map), ets:insert(Tab, {{Opt, Host}, Val}); (_) -> ok end, Defaults). -spec defaults() -> [atom() | {atom(), term()}]. defaults() -> lists:foldl( fun(Mod, Acc) -> lists:foldl( fun({Opt, Val}, Acc1) -> lists:keystore(Opt, 1, Acc1, {Opt, Val}); (Opt, Acc1) -> case lists:member(Opt, Acc1) of true -> Acc1; false -> [Opt|Acc1] end end, Acc, Mod:options()) end, ejabberd_options:options(), callback_modules(external)). -spec globals() -> [atom()]. globals() -> lists:usort( lists:flatmap( fun(Mod) -> case erlang:function_exported(Mod, globals, 0) of true -> Mod:globals(); false -> [] end end, callback_modules(all))). %% The module validator depends on virtual host, so we have to %% validate modules in this separate function. -spec validate_modules([binary()]) -> {ok, list()} | error_return(). validate_modules(Hosts) -> lists:foldl( fun(Host, {ok, Acc}) -> set_option(host, Host), ModOpts = get_option({modules, Host}), case gen_mod:validate(Host, ModOpts) of {ok, ModOpts1} -> {ok, [{{modules, Host}, ModOpts1}|Acc]}; Err -> Err end; (_, Err) -> Err 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). -spec compute_default(fun((global | binary()) -> T) | T, global | binary()) -> T. compute_default(F, Host) when is_function(F, 1) -> F(Host); 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 nocookie -> str:sha(p1_rand:get_string()); Cookie -> str:sha(erlang:atom_to_binary(Cookie, latin1)) 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).