diff options
Diffstat (limited to 'src/econf.erl')
-rw-r--r-- | src/econf.erl | 529 |
1 files changed, 529 insertions, 0 deletions
diff --git a/src/econf.erl b/src/econf.erl new file mode 100644 index 000000000..ac8b9ca6d --- /dev/null +++ b/src/econf.erl @@ -0,0 +1,529 @@ +%%%---------------------------------------------------------------------- +%%% File : econf.erl +%%% Purpose : Validator for ejabberd configuration options +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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]). +%% 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]). +-export([string/0, string/1]). +-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, glob/0]). +-export([path/0, binary_sep/1]). +-export([beam/0, beam/1]). +-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, well_known/2]). +-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("~s: unknown ~s: ~s. Did you mean ~s?", + [yconf:format_ctx(Ctx), + format_module_type(Ctx), Mod, + 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; + _ -> false + end + end, ejabberd_config:beams(all)), + format("~s: unknown ~s: ~s. Did you mean ~s?", + [yconf:format_ctx(Ctx), + format_module_type(Ctx), Mod, + 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("~s: '~s' is not a ~s", [Slogan, Mod, Type]); + false -> + case lists:member(Mod, ejabberd_config:beams(external)) of + true -> + format("~s: third-party ~s '~s' doesn't export " + "function ~s/~B. If it's really a ~s, " + "consider to upgrade it", + [Slogan, Type, Mod, F, A, Type]); + false -> + format("~s: '~s' doesn't match any known ~s", + [Slogan, Mod, Type]) + end + end; +format_error({unknown_option, [], _} = Why, Ctx) -> + format("~s. There are no available options", + [yconf:format_error(Why, Ctx)]); +format_error({unknown_option, Known, Opt} = Why, Ctx) -> + format("~s. Did you mean ~s? ~s", + [yconf:format_error(Why, Ctx), + misc:best_match(Opt, Known), + format_known("Available options", Known)]); +format_error({bad_enum, Known, Bad} = Why, Ctx) -> + format("~s. Did you mean ~s? ~s", + [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) -> + [H|T] = format_error(Reason), + yconf:format_ctx(Ctx) ++ ": " ++ [string:to_lower(H)|T]. + +format_error({bad_db_type, _, Atom}) -> + format("unsupported database: ~s", [Atom]); +format_error({bad_lang, Lang}) -> + format("Invalid language tag: ~s", [Lang]); +format_error({bad_pem, Why, Path}) -> + format("Failed to read PEM file '~s': ~s", + [Path, pkix:format_error(Why)]); +format_error({bad_cert, Why, Path}) -> + format_error({bad_pem, Why, Path}); +format_error({bad_jid, Bad}) -> + format("Invalid XMPP address: ~s", [Bad]); +format_error({bad_user, Bad}) -> + format("Invalid user part: ~s", [Bad]); +format_error({bad_domain, Bad}) -> + format("Invalid domain: ~s", [Bad]); +format_error({bad_resource, Bad}) -> + format("Invalid resource part: ~s", [Bad]); +format_error({bad_ldap_filter, Bad}) -> + format("Invalid LDAP filter: ~s", [Bad]); +format_error({bad_sip_uri, Bad}) -> + format("Invalid SIP URI: ~s", [Bad]); +format_error({route_conflict, R}) -> + format("Failed to reuse route '~s' because it's " + "already registered on a virtual host", + [R]); +format_error({listener_dup, AddrPort}) -> + format("Overlapping listeners found at ~s", + [format_addr_port(AddrPort)]); +format_error({listener_conflict, AddrPort1, AddrPort2}) -> + format("Overlapping listeners found at ~s and ~s", + [format_addr_port(AddrPort1), + format_addr_port(AddrPort2)]); +format_error({invalid_syntax, Reason}) -> + format("~s", [Reason]); +format_error({missing_module_dep, Mod, DepMod}) -> + format("module ~s depends on module ~s, " + "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). + +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), <<", ">>). + +%%%=================================================================== +%%% 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). + +enum(L) -> + yconf:enum(L). + +bool() -> + yconf:bool(). + +atom() -> + yconf:atom(). + +string() -> + yconf:string(). + +string(Re) -> + yconf:string(Re). + +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(). + +glob() -> + yconf:glob(). + +path() -> + yconf:path(). + +binary_sep(Sep) -> + yconf:binary_sep(Sep). + +beam() -> + yconf:beam(). + +beam(Exports) -> + yconf:beam(Exports). + +timeout(Units) -> + yconf:timeout(Units). + +timeout(Units, Inf) -> + yconf:timeout(Units, Inf). + +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 +%%%=================================================================== +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). + +well_known(queue_type, _) -> + queue_type(); +well_known(db_type, M) -> + db_type(M); +well_known(ram_db_type, M) -> + db_type(M); +well_known(cache_life_time, _) -> + pos_int(infinity); +well_known(cache_size, _) -> + pos_int(infinity); +well_known(use_cache, _) -> + bool(); +well_known(cache_missed, _) -> + bool(); +well_known(host, _) -> + host(); +well_known(hosts, _) -> + list(host(), [unique]). + +-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. + +%%%=================================================================== +%%% 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)). |