Packages
ejabberd
20.12.0
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/econf.erl
%%%----------------------------------------------------------------------
%%% File : econf.erl
%%% Purpose : Validator for ejabberd configuration options
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2020 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(econf).
%% API
-export([parse/3, validate/2, fail/1, format_error/2, replace_macros/1]).
-export([group_dups/1]).
%% Simple types
-export([pos_int/0, pos_int/1, non_neg_int/0, non_neg_int/1]).
-export([int/0, int/2, number/1, octal/0]).
-export([binary/0, binary/1, binary/2]).
-export([string/0, string/1, string/2]).
-export([enum/1, bool/0, atom/0, any/0]).
%% Complex types
-export([url/0, url/1]).
-export([file/0, file/1]).
-export([directory/0, directory/1]).
-export([ip/0, ipv4/0, ipv6/0, ip_mask/0, port/0]).
-export([re/0, re/1, glob/0, glob/1]).
-export([path/0, binary_sep/1]).
-export([beam/0, beam/1, base64/0]).
-export([timeout/1, timeout/2]).
%% Composite types
-export([list/1, list/2]).
-export([list_or_single/1, list_or_single/2]).
-export([map/2, map/3]).
-export([either/2, and_then/2, non_empty/1]).
-export([options/1, options/2]).
%% Custom types
-export([acl/0, shaper/0, url_or_file/0, lang/0]).
-export([pem/0, queue_type/0]).
-export([jid/0, user/0, domain/0, resource/0]).
-export([db_type/1, ldap_filter/0]).
-export([host/0, hosts/0]).
-export([vcard_temp/0]).
-ifdef(SIP).
-export([sip_uri/0]).
-endif.
-type error_reason() :: term().
-type error_return() :: {error, error_reason(), yconf:ctx()}.
-type validator() :: yconf:validator().
-type validator(T) :: yconf:validator(T).
-type validators() :: yconf:validators().
-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} ->
{error, Reason, Ctx}
end.
validate(Validator, Y) ->
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] ->
Mods = ejabberd_config:beams(all),
format("~ts: unknown ~ts: ~ts. Did you mean ~ts?",
[yconf:format_ctx(Ctx),
format_module_type(Ctx),
format_module(Mod),
format_module(misc:best_match(Mod, Mods))]);
format_error({bad_module, Mod}, Ctx)
when Ctx == [modules] ->
Mods = lists:filter(
fun(M) ->
case atom_to_list(M) of
"mod_" ++ _ -> true;
"Elixir.Mod" ++ _ -> true;
_ -> false
end
end, ejabberd_config:beams(all)),
format("~ts: unknown ~ts: ~ts. Did you mean ~ts?",
[yconf:format_ctx(Ctx),
format_module_type(Ctx),
format_module(Mod),
format_module(misc:best_match(Mod, Mods))]);
format_error({bad_export, {F, A}, Mod}, Ctx)
when Ctx == [listen, module];
Ctx == [listen, request_handlers];
Ctx == [modules] ->
Type = format_module_type(Ctx),
Slogan = yconf:format_ctx(Ctx),
case lists:member(Mod, ejabberd_config:beams(local)) of
true ->
format("~ts: '~ts' is not a ~ts",
[Slogan, format_module(Mod), Type]);
false ->
case lists:member(Mod, ejabberd_config:beams(external)) of
true ->
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]);
false ->
format("~ts: '~ts' doesn't match any known ~ts",
[Slogan, format_module(Mod), Type])
end
end;
format_error({unknown_option, [], _} = Why, Ctx) ->
format("~ts. There are no available options",
[yconf:format_error(Why, Ctx)]);
format_error({unknown_option, Known, Opt} = Why, Ctx) ->
format("~ts. Did you mean ~ts? ~ts",
[yconf:format_error(Why, Ctx),
misc:best_match(Opt, Known),
format_known("Available options", Known)]);
format_error({bad_enum, Known, Bad} = Why, Ctx) ->
format("~ts. Did you mean ~ts? ~ts",
[yconf:format_error(Why, Ctx),
misc:best_match(Bad, Known),
format_known("Possible values", Known)]);
format_error({bad_yaml, _, _} = Why, _) ->
format_error(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}) ->
format("Invalid language tag: ~ts", [Lang]);
format_error({bad_pem, Why, Path}) ->
format("Failed to read PEM file '~ts': ~ts",
[Path, pkix:format_error(Why)]);
format_error({bad_cert, Why, Path}) ->
format_error({bad_pem, Why, Path});
format_error({bad_jwt_key, Path}) ->
format("No valid JWT key found in file: ~ts", [Path]);
format_error({bad_jwt_key_set, Path}) ->
format("JWK set contains multiple JWT keys in file: ~ts", [Path]);
format_error({bad_jid, Bad}) ->
format("Invalid XMPP address: ~ts", [Bad]);
format_error({bad_user, Bad}) ->
format("Invalid user part: ~ts", [Bad]);
format_error({bad_domain, Bad}) ->
format("Invalid domain: ~ts", [Bad]);
format_error({bad_resource, Bad}) ->
format("Invalid resource part: ~ts", [Bad]);
format_error({bad_ldap_filter, Bad}) ->
format("Invalid LDAP filter: ~ts", [Bad]);
format_error({bad_sip_uri, Bad}) ->
format("Invalid SIP URI: ~ts", [Bad]);
format_error({route_conflict, R}) ->
format("Failed to reuse route '~ts' because it's "
"already registered on a virtual host",
[R]);
format_error({listener_dup, AddrPort}) ->
format("Overlapping listeners found at ~ts",
[format_addr_port(AddrPort)]);
format_error({listener_conflict, AddrPort1, AddrPort2}) ->
format("Overlapping listeners found at ~ts and ~ts",
[format_addr_port(AddrPort1),
format_addr_port(AddrPort2)]);
format_error({invalid_syntax, Reason}) ->
format("~ts", [Reason]);
format_error({missing_module_dep, Mod, DepMod}) ->
format("module ~ts depends on module ~ts, "
"which is not found in the config",
[Mod, DepMod]);
format_error(eimp_error) ->
format("ejabberd is built without image converter support", []);
format_error({mqtt_codec, Reason}) ->
mqtt_codec:format_error(Reason);
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));
format_module(Mod) ->
case Mod of
"Elixir." ++ M -> M;
M -> M
end.
format_module_type([listen, module]) ->
"listening module";
format_module_type([listen, request_handlers]) ->
"HTTP request handler";
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(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).
group_dups(Y1) ->
lists:reverse(
lists:foldl(
fun({Option, Values}, Acc) when is_list(Values) ->
case lists:keyfind(Option, 1, Acc) of
{Option, Vals} when is_list(Vals) ->
lists:keyreplace(Option, 1, Acc, {Option, Vals ++ Values});
_ ->
[{Option, Values}|Acc]
end;
(Other, Acc) ->
[Other|Acc]
end, [], Y1)).
%%%===================================================================
%%% Validators from yconf
%%%===================================================================
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()),
fun(<<"Elixir.", _/binary>> = Val) ->
(yconf:beam(Exports))(Val);
(<<C, _/binary>> = Val) when C >= $A, C =< $Z ->
(yconf:beam(Exports))(<<"Elixir.", Val/binary>>);
(Val) ->
(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})
end
end).
-spec pem() -> yconf:validator(binary()).
pem() ->
and_then(
path(),
fun(Path) ->
case pkix:is_pem_file(Path) of
true -> Path;
{false, Reason} ->
fail({bad_pem, Reason, Path})
end
end).
-spec jid() -> yconf:validator(jid:jid()).
jid() ->
and_then(
binary(),
fun(Val) ->
try jid:decode(Val)
catch _:{bad_jid, _} = Reason -> fail(Reason)
end
end).
-spec user() -> yconf:validator(binary()).
user() ->
and_then(
binary(),
fun(Val) ->
case jid:nodeprep(Val) of
error -> fail({bad_user, Val});
U -> U
end
end).
-spec domain() -> yconf:validator(binary()).
domain() ->
and_then(
non_empty(binary()),
fun(Val) ->
try jid:tolower(jid:decode(Val)) of
{<<"">>, Domain, <<"">>} -> Domain;
_ -> fail({bad_domain, Val})
catch _:{bad_jid, _} ->
fail({bad_domain, Val})
end
end).
-spec resource() -> yconf:validator(binary()).
resource() ->
and_then(
binary(),
fun(Val) ->
case jid:resourceprep(Val) of
error -> fail({bad_resource, Val});
R -> R
end
end).
-spec db_type(module()) -> yconf:validator(atom()).
db_type(M) ->
and_then(
atom(),
fun(T) ->
case code:ensure_loaded(db_module(M, T)) of
{module, _} -> T;
{error, _} -> fail({bad_db_type, M, T})
end
end).
-spec queue_type() -> yconf:validator(ram | file).
queue_type() ->
enum([ram, file]).
-spec ldap_filter() -> yconf:validator(binary()).
ldap_filter() ->
and_then(
binary(),
fun(Val) ->
case eldap_filter:parse(Val) of
{ok, _} -> Val;
_ -> fail({bad_ldap_filter, Val})
end
end).
-ifdef(SIP).
sip_uri() ->
and_then(
binary(),
fun(Val) ->
case esip:decode_uri(Val) of
error -> fail({bad_sip_uri, Val});
URI -> URI
end
end).
-endif.
-spec host() -> yconf:validator(binary()).
host() ->
fun(Domain) ->
Host = ejabberd_config:get_myname(),
Hosts = ejabberd_config:get_option(hosts),
Domain1 = (binary())(Domain),
Domain2 = misc:expand_keyword(<<"@HOST@">>, Domain1, Host),
Domain3 = (domain())(Domain2),
case lists:member(Domain3, Hosts) of
true -> fail({route_conflict, Domain3});
false -> Domain3
end
end.
-spec hosts() -> yconf:validator([binary()]).
hosts() ->
list(host(), [unique]).
-spec vcard_temp() -> yconf:validator().
vcard_temp() ->
vcard_validator(
vcard_temp, undefined,
[{version, undefined, binary()},
{fn, undefined, binary()},
{n, undefined, vcard_name()},
{nickname, undefined, binary()},
{photo, undefined, vcard_photo()},
{bday, undefined, binary()},
{adr, [], list(vcard_adr())},
{label, [], list(vcard_label())},
{tel, [], list(vcard_tel())},
{email, [], list(vcard_email())},
{jabberid, undefined, binary()},
{mailer, undefined, binary()},
{tz, undefined, binary()},
{geo, undefined, vcard_geo()},
{title, undefined, binary()},
{role, undefined, binary()},
{logo, undefined, vcard_logo()},
{org, undefined, vcard_org()},
{categories, [], list(binary())},
{note, undefined, binary()},
{prodid, undefined, binary()},
{rev, undefined, binary()},
{sort_string, undefined, binary()},
{sound, undefined, vcard_sound()},
{uid, undefined, binary()},
{url, undefined, binary()},
{class, undefined, enum([confidential, private, public])},
{key, undefined, vcard_key()},
{desc, undefined, binary()}]).
-spec vcard_name() -> yconf:validator().
vcard_name() ->
vcard_validator(
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,
[{type, undefined, binary()},
{binval, undefined, base64()},
{extval, undefined, binary()}]).
-spec vcard_adr() -> yconf:validator().
vcard_adr() ->
vcard_validator(
vcard_adr, [],
[{home, false, bool()},
{work, false, bool()},
{postal, false, bool()},
{parcel, false, bool()},
{dom, false, bool()},
{intl, false, bool()},
{pref, false, bool()},
{pobox, undefined, binary()},
{extadd, undefined, binary()},
{street, undefined, binary()},
{locality, undefined, binary()},
{region, undefined, binary()},
{pcode, undefined, binary()},
{ctry, undefined, binary()}]).
-spec vcard_label() -> yconf:validator().
vcard_label() ->
vcard_validator(
vcard_label, [],
[{home, false, bool()},
{work, false, bool()},
{postal, false, bool()},
{parcel, false, bool()},
{dom, false, bool()},
{intl, false, bool()},
{pref, false, bool()},
{line, [], list(binary())}]).
-spec vcard_tel() -> yconf:validator().
vcard_tel() ->
vcard_validator(
vcard_tel, [],
[{home, false, bool()},
{work, false, bool()},
{voice, false, bool()},
{fax, false, bool()},
{pager, false, bool()},
{msg, false, bool()},
{cell, false, bool()},
{video, false, bool()},
{bbs, false, bool()},
{modem, false, bool()},
{isdn, false, bool()},
{pcs, false, bool()},
{pref, false, bool()},
{number, undefined, binary()}]).
-spec vcard_email() -> yconf:validator().
vcard_email() ->
vcard_validator(
vcard_email, [],
[{home, false, bool()},
{work, false, bool()},
{internet, false, bool()},
{pref, false, bool()},
{x400, false, bool()},
{userid, undefined, binary()}]).
-spec vcard_geo() -> yconf:validator().
vcard_geo() ->
vcard_validator(
vcard_geo, undefined,
[{lat, undefined, binary()},
{lon, undefined, binary()}]).
-spec vcard_logo() -> yconf:validator().
vcard_logo() ->
vcard_validator(
vcard_logo, undefined,
[{type, undefined, binary()},
{binval, undefined, base64()},
{extval, undefined, binary()}]).
-spec vcard_org() -> yconf:validator().
vcard_org() ->
vcard_validator(
vcard_org, undefined,
[{name, undefined, binary()},
{units, [], list(binary())}]).
-spec vcard_sound() -> yconf:validator().
vcard_sound() ->
vcard_validator(
vcard_sound, undefined,
[{phonetic, undefined, binary()},
{binval, undefined, base64()},
{extval, undefined, binary()}]).
-spec vcard_key() -> yconf:validator().
vcard_key() ->
vcard_validator(
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 ->
fail({bad_length, 255})
end.
format_addr_port({IP, Port}) ->
IPStr = case tuple_size(IP) of
4 -> inet:ntoa(IP);
8 -> "[" ++ inet:ntoa(IP) ++ "]"
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],
and_then(
options(
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]]).