%%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov %%% @doc %%% This is the place for some unsorted auxiliary functions %%% Some functions from jlib.erl are moved here %%% Mild rubbish heap is accepted ;) %%% @end %%% Created : 30 Mar 2017 by Evgeny Khramtsov %%% %%% %%% 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(misc). %% API -export([add_delay_info/3, add_delay_info/4, unwrap_carbon/1, unwrap_mucsub_message/1, is_standalone_chat_state/1, tolower/1, term_to_base64/1, base64_to_term/1, ip_to_list/1, hex_to_bin/1, hex_to_base64/1, url_encode/1, expand_keyword/3, atom_to_binary/1, binary_to_atom/1, tuple_to_binary/1, l2i/1, i2l/1, i2l/2, expr_to_term/1, term_to_expr/1, now_to_usec/1, usec_to_now/1, encode_pid/1, decode_pid/2, compile_exprs/2, join_atoms/2, try_read_file/1, get_descr/2, css_dir/0, img_dir/0, js_dir/0, msgs_dir/0, sql_dir/0, lua_dir/0, read_css/1, read_img/1, read_js/1, read_lua/1, intersection/2, format_val/1, cancel_timer/1, unique_timestamp/0, is_mucsub_message/1, best_match/2, pmap/2, peach/2, format_exception/4, get_my_ipv4_address/0, get_my_ipv6_address/0, parse_ip_mask/1, crypto_hmac/3, crypto_hmac/4, uri_parse/1, match_ip_mask/3, format_hosts_list/1, format_cycle/1, delete_dir/1]). %% Deprecated functions -export([decode_base64/1, encode_base64/1]). -deprecated([{decode_base64, 1}, {encode_base64, 1}]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include_lib("kernel/include/file.hrl"). -type distance_cache() :: #{{string(), string()} => non_neg_integer()}. -ifdef(USE_OLD_HTTP_URI). uri_parse(URL) when is_binary(URL) -> uri_parse(binary_to_list(URL)); uri_parse(URL) -> {ok, {Scheme, _UserInfo, Host, Port, Path, _Query}} = http_uri:parse(URL), {ok, Scheme, Host, Port, Path}. -else. uri_parse(URL) when is_binary(URL) -> uri_parse(binary_to_list(URL)); uri_parse(URL) -> case uri_string:parse(URL) of #{scheme := Scheme, host := Host, port := Port, path := Path} -> {ok, Scheme, Host, Port, Path}; #{scheme := "https", host := Host, path := Path} -> {ok, "https", Host, 443, Path}; #{scheme := "http", host := Host, path := Path} -> {ok, "http", Host, 80, Path} end. -endif. -ifdef(USE_OLD_CRYPTO_HMAC). crypto_hmac(Type, Key, Data) -> crypto:hmac(Type, Key, Data). crypto_hmac(Type, Key, Data, MacL) -> crypto:hmac(Type, Key, Data, MacL). -else. crypto_hmac(Type, Key, Data) -> crypto:mac(hmac, Type, Key, Data). crypto_hmac(Type, Key, Data, MacL) -> crypto:macN(hmac, Type, Key, Data, MacL). -endif. %%%=================================================================== %%% API %%%=================================================================== -spec add_delay_info(stanza(), jid(), erlang:timestamp()) -> stanza(). add_delay_info(Stz, From, Time) -> add_delay_info(Stz, From, Time, <<"">>). -spec add_delay_info(stanza(), jid(), erlang:timestamp(), binary()) -> stanza(). add_delay_info(Stz, From, Time, Desc) -> Delays = xmpp:get_subtags(Stz, #delay{stamp = {0,0,0}}), Matching = lists:any( fun(#delay{from = OldFrom}) when is_record(OldFrom, jid) -> jid:tolower(From) == jid:tolower(OldFrom); (_) -> false end, Delays), case Matching of true -> Stz; _ -> NewDelay = #delay{stamp = Time, from = From, desc = Desc}, xmpp:append_subtags(Stz, [NewDelay]) end. -spec unwrap_carbon(stanza()) -> xmpp_element(). unwrap_carbon(#message{} = Msg) -> try case xmpp:get_subtag(Msg, #carbons_sent{forwarded = #forwarded{}}) of #carbons_sent{forwarded = #forwarded{sub_els = [El]}} -> xmpp:decode(El, ?NS_CLIENT, [ignore_els]); _ -> case xmpp:get_subtag(Msg, #carbons_received{ forwarded = #forwarded{}}) of #carbons_received{forwarded = #forwarded{sub_els = [El]}} -> xmpp:decode(El, ?NS_CLIENT, [ignore_els]); _ -> Msg end end catch _:{xmpp_codec, _} -> Msg end; unwrap_carbon(Stanza) -> Stanza. -spec unwrap_mucsub_message(xmpp_element()) -> message() | false. unwrap_mucsub_message(#message{} = OuterMsg) -> case xmpp:get_subtag(OuterMsg, #ps_event{}) of #ps_event{ items = #ps_items{ node = Node, items = [ #ps_item{ sub_els = [#message{} = InnerMsg]} | _]}} when Node == ?NS_MUCSUB_NODES_MESSAGES; Node == ?NS_MUCSUB_NODES_SUBJECT -> InnerMsg; _ -> false end; unwrap_mucsub_message(_Packet) -> false. -spec is_mucsub_message(xmpp_element()) -> boolean(). is_mucsub_message(#message{} = OuterMsg) -> case xmpp:get_subtag(OuterMsg, #ps_event{}) of #ps_event{ items = #ps_items{ node = Node}} when Node == ?NS_MUCSUB_NODES_MESSAGES; Node == ?NS_MUCSUB_NODES_SUBJECT; Node == ?NS_MUCSUB_NODES_AFFILIATIONS; Node == ?NS_MUCSUB_NODES_CONFIG; Node == ?NS_MUCSUB_NODES_PARTICIPANTS; Node == ?NS_MUCSUB_NODES_PRESENCE; Node == ?NS_MUCSUB_NODES_SUBSCRIBERS -> true; _ -> false end; is_mucsub_message(_Packet) -> false. -spec is_standalone_chat_state(stanza()) -> boolean(). is_standalone_chat_state(Stanza) -> case unwrap_carbon(Stanza) of #message{body = [], subject = [], sub_els = Els} -> IgnoreNS = [?NS_CHATSTATES, ?NS_DELAY, ?NS_EVENT], Stripped = [El || El <- Els, not lists:member(xmpp:get_ns(El), IgnoreNS)], Stripped == []; _ -> false end. -spec tolower(binary()) -> binary(). tolower(B) -> iolist_to_binary(tolower_s(binary_to_list(B))). tolower_s([C | Cs]) -> if C >= $A, C =< $Z -> [C + 32 | tolower_s(Cs)]; true -> [C | tolower_s(Cs)] end; tolower_s([]) -> []. -spec term_to_base64(term()) -> binary(). term_to_base64(Term) -> encode_base64(term_to_binary(Term)). -spec base64_to_term(binary()) -> {term, term()} | error. base64_to_term(Base64) -> try binary_to_term(base64:decode(Base64), [safe]) of Term -> {term, Term} catch _:_ -> error end. -spec decode_base64(binary()) -> binary(). decode_base64(S) -> try base64:mime_decode(S) catch _:badarg -> <<>> end. -spec encode_base64(binary()) -> binary(). encode_base64(Data) -> base64:encode(Data). -spec ip_to_list(inet:ip_address() | undefined | {inet:ip_address(), inet:port_number()}) -> binary(). ip_to_list({local, _}) -> <<"unix">>; ip_to_list({IP, _Port}) -> ip_to_list(IP); %% This function clause could use inet_parse too: ip_to_list(undefined) -> <<"unknown">>; ip_to_list(IP) -> list_to_binary(inet_parse:ntoa(IP)). -spec hex_to_bin(binary()) -> binary(). hex_to_bin(Hex) -> hex_to_bin(binary_to_list(Hex), []). -spec hex_to_bin(list(), list()) -> binary(). hex_to_bin([], Acc) -> list_to_binary(lists:reverse(Acc)); hex_to_bin([H1, H2 | T], Acc) -> {ok, [V], []} = io_lib:fread("~16u", [H1, H2]), hex_to_bin(T, [V | Acc]). -spec hex_to_base64(binary()) -> binary(). hex_to_base64(Hex) -> base64:encode(hex_to_bin(Hex)). -spec url_encode(binary()) -> binary(). url_encode(A) -> url_encode(A, <<>>). -spec expand_keyword(iodata(), iodata(), iodata()) -> binary(). expand_keyword(Keyword, Input, Replacement) -> re:replace(Input, Keyword, Replacement, [{return, binary}, global]). binary_to_atom(Bin) -> erlang:binary_to_atom(Bin, utf8). tuple_to_binary(T) -> iolist_to_binary(tuple_to_list(T)). atom_to_binary(A) -> erlang:atom_to_binary(A, utf8). expr_to_term(Expr) -> Str = binary_to_list(<>), {ok, Tokens, _} = erl_scan:string(Str), {ok, Term} = erl_parse:parse_term(Tokens), Term. term_to_expr(Term) -> list_to_binary(io_lib:print(Term)). -spec now_to_usec(erlang:timestamp()) -> non_neg_integer(). now_to_usec({MSec, Sec, USec}) -> (MSec*1000000 + Sec)*1000000 + USec. -spec usec_to_now(non_neg_integer()) -> erlang:timestamp(). usec_to_now(Int) -> Secs = Int div 1000000, USec = Int rem 1000000, MSec = Secs div 1000000, Sec = Secs rem 1000000, {MSec, Sec, USec}. l2i(I) when is_integer(I) -> I; l2i(L) when is_binary(L) -> binary_to_integer(L). i2l(I) when is_integer(I) -> integer_to_binary(I); i2l(L) when is_binary(L) -> L. i2l(I, N) when is_integer(I) -> i2l(i2l(I), N); i2l(L, N) when is_binary(L) -> case str:len(L) of N -> L; C when C > N -> L; _ -> i2l(<<$0, L/binary>>, N) end. -spec encode_pid(pid()) -> binary(). encode_pid(Pid) -> list_to_binary(erlang:pid_to_list(Pid)). -spec decode_pid(binary(), binary()) -> pid(). decode_pid(PidBin, NodeBin) -> PidStr = binary_to_list(PidBin), Pid = erlang:list_to_pid(PidStr), case erlang:binary_to_atom(NodeBin, latin1) of Node when Node == node() -> Pid; Node -> try set_node_id(PidStr, NodeBin) catch _:badarg -> erlang:error({bad_node, Node}) end end. -spec compile_exprs(module(), [string()]) -> ok | {error, any()}. compile_exprs(Mod, Exprs) -> try Forms = lists:map( fun(Expr) -> {ok, Tokens, _} = erl_scan:string(lists:flatten(Expr)), {ok, Form} = erl_parse:parse_form(Tokens), Form end, Exprs), {ok, Code} = case compile:forms(Forms, []) of {ok, Mod, Bin} -> {ok, Bin}; {ok, Mod, Bin, _Warnings} -> {ok, Bin}; Error -> Error end, {module, Mod} = code:load_binary(Mod, "nofile", Code), ok catch _:{badmatch, {error, ErrInfo, _ErrLocation}} -> {error, ErrInfo}; _:{badmatch, {error, _} = Err} -> Err; _:{badmatch, error} -> {error, compile_failed} end. -spec join_atoms([atom()], binary()) -> binary(). join_atoms(Atoms, Sep) -> str:join([io_lib:format("~p", [A]) || A <- lists:sort(Atoms)], Sep). %% @doc Checks if the file is readable and converts its name to binary. %% Fails with `badarg' otherwise. The function is intended for usage %% in configuration validators only. -spec try_read_file(file:filename_all()) -> binary(). try_read_file(Path) -> case file:open(Path, [read]) of {ok, Fd} -> file:close(Fd), iolist_to_binary(Path); {error, Why} -> ?ERROR_MSG("Failed to read ~ts: ~ts", [Path, file:format_error(Why)]), erlang:error(badarg) end. -spec css_dir() -> file:filename(). css_dir() -> get_dir("css"). -spec img_dir() -> file:filename(). img_dir() -> get_dir("img"). -spec js_dir() -> file:filename(). js_dir() -> get_dir("js"). -spec msgs_dir() -> file:filename(). msgs_dir() -> get_dir("msgs"). -spec sql_dir() -> file:filename(). sql_dir() -> get_dir("sql"). -spec lua_dir() -> file:filename(). lua_dir() -> get_dir("lua"). -spec read_css(file:filename()) -> {ok, binary()} | {error, file:posix()}. read_css(File) -> read_file(filename:join(css_dir(), File)). -spec read_img(file:filename()) -> {ok, binary()} | {error, file:posix()}. read_img(File) -> read_file(filename:join(img_dir(), File)). -spec read_js(file:filename()) -> {ok, binary()} | {error, file:posix()}. read_js(File) -> read_file(filename:join(js_dir(), File)). -spec read_lua(file:filename()) -> {ok, binary()} | {error, file:posix()}. read_lua(File) -> read_file(filename:join(lua_dir(), File)). -spec get_descr(binary(), binary()) -> binary(). get_descr(Lang, Text) -> Desc = translate:translate(Lang, Text), Copyright = ejabberd_config:get_copyright(), <>. -spec intersection(list(), list()) -> list(). intersection(L1, L2) -> lists:filter( fun(E) -> lists:member(E, L2) end, L1). -spec format_val(any()) -> iodata(). format_val({yaml, S}) when is_integer(S); is_binary(S); is_atom(S) -> format_val(S); format_val({yaml, YAML}) -> S = try fast_yaml:encode(YAML) catch _:_ -> YAML end, format_val(S); format_val(I) when is_integer(I) -> integer_to_list(I); format_val(B) when is_atom(B) -> erlang:atom_to_binary(B, utf8); format_val(Term) -> S = try iolist_to_binary(Term) catch _:_ -> list_to_binary(io_lib:format("~p", [Term])) end, case binary:match(S, <<"\n">>) of nomatch -> S; _ -> [io_lib:nl(), S] end. -spec cancel_timer(reference() | undefined) -> ok. cancel_timer(TRef) when is_reference(TRef) -> case erlang:cancel_timer(TRef) of false -> receive {timeout, TRef, _} -> ok after 0 -> ok end; _ -> ok end; cancel_timer(_) -> ok. -spec best_match(atom() | binary() | string(), [atom() | binary() | string()]) -> string(). best_match(Pattern, []) -> Pattern; best_match(Pattern, Opts) -> String = to_string(Pattern), {Ds, _} = lists:mapfoldl( fun(Opt, Cache) -> SOpt = to_string(Opt), {Distance, Cache1} = ld(String, SOpt, Cache), {{Distance, SOpt}, Cache1} end, #{}, Opts), element(2, lists:min(Ds)). -spec pmap(fun((T1) -> T2), [T1]) -> [T2]. pmap(Fun, [_,_|_] = List) -> case erlang:system_info(logical_processors) of 1 -> lists:map(Fun, List); _ -> Self = self(), lists:map( fun({Pid, Ref}) -> receive {Pid, Ret} -> receive {'DOWN', Ref, _, _, _} -> Ret end; {'DOWN', Ref, _, _, Reason} -> exit(Reason) end end, [spawn_monitor( fun() -> Self ! {self(), Fun(X)} end) || X <- List]) end; pmap(Fun, List) -> lists:map(Fun, List). -spec peach(fun((T) -> any()), [T]) -> ok. peach(Fun, [_,_|_] = List) -> case erlang:system_info(logical_processors) of 1 -> lists:foreach(Fun, List); _ -> Self = self(), lists:foreach( fun({Pid, Ref}) -> receive Pid -> receive {'DOWN', Ref, _, _, _} -> ok end; {'DOWN', Ref, _, _, Reason} -> exit(Reason) end end, [spawn_monitor( fun() -> Fun(X), Self ! self() end) || X <- List]) end; peach(Fun, List) -> lists:foreach(Fun, List). -ifdef(HAVE_ERL_ERROR). format_exception(Level, Class, Reason, Stacktrace) -> erl_error:format_exception( Level, Class, Reason, Stacktrace, fun(_M, _F, _A) -> false end, fun(Term, I) -> io_lib:print(Term, I, 80, -1) end). -else. format_exception(Level, Class, Reason, Stacktrace) -> lib:format_exception( Level, Class, Reason, Stacktrace, fun(_M, _F, _A) -> false end, fun(Term, I) -> io_lib:print(Term, I, 80, -1) end). -endif. -spec get_my_ipv4_address() -> inet:ip4_address(). get_my_ipv4_address() -> {ok, MyHostName} = inet:gethostname(), case inet:getaddr(MyHostName, inet) of {ok, Addr} -> Addr; {error, _} -> {127, 0, 0, 1} end. -spec get_my_ipv6_address() -> inet:ip6_address(). get_my_ipv6_address() -> {ok, MyHostName} = inet:gethostname(), case inet:getaddr(MyHostName, inet6) of {ok, Addr} -> Addr; {error, _} -> {0, 0, 0, 0, 0, 0, 0, 1} end. -spec parse_ip_mask(binary()) -> {ok, {inet:ip4_address(), 0..32}} | {ok, {inet:ip6_address(), 0..128}} | error. parse_ip_mask(S) -> case econf:validate(econf:ip_mask(), S) of {ok, _} = Ret -> Ret; _ -> error end. -spec match_ip_mask(inet:ip_address(), inet:ip_address(), 0..128) -> boolean(). match_ip_mask({_, _, _, _} = IP, {_, _, _, _} = Net, Mask) -> IPInt = ip_to_integer(IP), NetInt = ip_to_integer(Net), M = bnot (1 bsl (32 - Mask) - 1), IPInt band M =:= NetInt band M; match_ip_mask({_, _, _, _, _, _, _, _} = IP, {_, _, _, _, _, _, _, _} = Net, Mask) -> IPInt = ip_to_integer(IP), NetInt = ip_to_integer(Net), M = bnot (1 bsl (128 - Mask) - 1), IPInt band M =:= NetInt band M; match_ip_mask({_, _, _, _} = IP, {0, 0, 0, 0, 0, 16#FFFF, _, _} = Net, Mask) -> IPInt = ip_to_integer({0, 0, 0, 0, 0, 16#FFFF, 0, 0}) + ip_to_integer(IP), NetInt = ip_to_integer(Net), M = bnot (1 bsl (128 - Mask) - 1), IPInt band M =:= NetInt band M; match_ip_mask({0, 0, 0, 0, 0, 16#FFFF, _, _} = IP, {_, _, _, _} = Net, Mask) -> IPInt = ip_to_integer(IP) - ip_to_integer({0, 0, 0, 0, 0, 16#FFFF, 0, 0}), NetInt = ip_to_integer(Net), M = bnot (1 bsl (32 - Mask) - 1), IPInt band M =:= NetInt band M; match_ip_mask(_, _, _) -> false. -spec format_hosts_list([binary(), ...]) -> iolist(). format_hosts_list([Host]) -> Host; format_hosts_list([H1, H2]) -> [H1, " and ", H2]; format_hosts_list([H1, H2, H3]) -> [H1, ", ", H2, " and ", H3]; format_hosts_list([H1, H2|Hs]) -> io_lib:format("~ts, ~ts and ~B more hosts", [H1, H2, length(Hs)]). -spec format_cycle([atom(), ...]) -> iolist(). format_cycle([M1]) -> atom_to_list(M1); format_cycle([M1, M2]) -> [atom_to_list(M1), " and ", atom_to_list(M2)]; format_cycle([M|Ms]) -> atom_to_list(M) ++ ", " ++ format_cycle(Ms). -spec delete_dir(file:filename_all()) -> ok | {error, file:posix()}. delete_dir(Dir) -> try {ok, Entries} = file:list_dir(Dir), lists:foreach(fun(Path) -> case filelib:is_dir(Path) of true -> ok = delete_dir(Path); false -> ok = file:delete(Path) end end, [filename:join(Dir, Entry) || Entry <- Entries]), ok = file:del_dir(Dir) catch _:{badmatch, {error, Error}} -> {error, Error} end. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec url_encode(binary(), binary()) -> binary(). url_encode(<>, Acc) when (H >= $a andalso H =< $z) orelse (H >= $A andalso H =< $Z) orelse (H >= $0 andalso H =< $9) orelse H == $_ orelse H == $. orelse H == $- orelse H == $/ orelse H == $: -> url_encode(T, <>); url_encode(<>, Acc) -> case integer_to_list(H, 16) of [X, Y] -> url_encode(T, <>); [X] -> url_encode(T, <>) end; url_encode(<<>>, Acc) -> Acc. -spec set_node_id(string(), binary()) -> pid(). set_node_id(PidStr, NodeBin) -> ExtPidStr = erlang:pid_to_list( binary_to_term( <<131,103,100,(size(NodeBin)):16,NodeBin/binary,0:72>>)), [H|_] = string:tokens(ExtPidStr, "."), [_|T] = string:tokens(PidStr, "."), erlang:list_to_pid(string:join([H|T], ".")). -spec read_file(file:filename()) -> {ok, binary()} | {error, file:posix()}. read_file(Path) -> case file:read_file(Path) of {ok, Data} -> {ok, Data}; {error, Why} = Err -> ?ERROR_MSG("Failed to read file ~ts: ~ts", [Path, file:format_error(Why)]), Err end. -spec get_dir(string()) -> file:filename(). get_dir(Type) -> Env = "EJABBERD_" ++ string:to_upper(Type) ++ "_PATH", case os:getenv(Env) of false -> case code:priv_dir(ejabberd) of {error, _} -> filename:join(["priv", Type]); Path -> filename:join([Path, Type]) end; Path -> Path end. %% Generates erlang:timestamp() that is guaranteed to unique -spec unique_timestamp() -> erlang:timestamp(). unique_timestamp() -> {MS, S, _} = erlang:timestamp(), {MS, S, erlang:unique_integer([positive, monotonic]) rem 1000000}. %% Levenshtein distance -spec ld(string(), string(), distance_cache()) -> {non_neg_integer(), distance_cache()}. ld([] = S, T, Cache) -> {length(T), maps:put({S, T}, length(T), Cache)}; ld(S, [] = T, Cache) -> {length(S), maps:put({S, T}, length(S), Cache)}; ld([X|S], [X|T], Cache) -> ld(S, T, Cache); ld([_|ST] = S, [_|TT] = T, Cache) -> try {maps:get({S, T}, Cache), Cache} catch _:{badkey, _} -> {L1, C1} = ld(S, TT, Cache), {L2, C2} = ld(ST, T, C1), {L3, C3} = ld(ST, TT, C2), L = 1 + lists:min([L1, L2, L3]), {L, maps:put({S, T}, L, C3)} end. -spec ip_to_integer(inet:ip_address()) -> non_neg_integer(). ip_to_integer({IP1, IP2, IP3, IP4}) -> IP1 bsl 8 bor IP2 bsl 8 bor IP3 bsl 8 bor IP4; ip_to_integer({IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8}) -> IP1 bsl 16 bor IP2 bsl 16 bor IP3 bsl 16 bor IP4 bsl 16 bor IP5 bsl 16 bor IP6 bsl 16 bor IP7 bsl 16 bor IP8. -spec to_string(atom() | binary() | string()) -> string(). to_string(A) when is_atom(A) -> atom_to_list(A); to_string(B) when is_binary(B) -> binary_to_list(B); to_string(S) -> S.