aboutsummaryrefslogtreecommitdiff
path: root/src/econf.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/econf.erl')
-rw-r--r--src/econf.erl741
1 files changed, 741 insertions, 0 deletions
diff --git a/src/econf.erl b/src/econf.erl
new file mode 100644
index 000000000..75817a641
--- /dev/null
+++ b/src/econf.erl
@@ -0,0 +1,741 @@
+%%%----------------------------------------------------------------------
+%%% 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]).
+-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]]).