1%%%=============================================================================
2%%% Copyright 2011, Travelping GmbH <info@travelping.com>
3%%% Copyright 2013-2017, Tobias Schlager <schlagert@github.com>
4%%%
5%%% Permission to use, copy, modify, and/or distribute this software for any
6%%% purpose with or without fee is hereby granted, provided that the above
7%%% copyright notice and this permission notice appear in all copies.
8%%%
9%%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10%%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11%%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12%%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13%%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14%%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15%%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16%%%
17%%% @doc
18%%% A library module providing `syslog' specific utility functions.
19%%% @end
20%%%=============================================================================
21-module(syslog_lib).
22
23%% API
24-export([get_hostname/1,
25         get_name/0,
26         get_name_metdata_key/0,
27         get_property/2,
28         get_property/3,
29         get_pid/1,
30         get_utc_datetime/1,
31         get_utc_offset/2,
32         truncate/2,
33         format_rfc3164_date/1,
34         format_rfc5424_date/1,
35         get_structured_data/3,
36         has_error_logger/0,
37         to_type/2]).
38
39-define(GET_ENV(Property), application:get_env(syslog, Property)).
40
41-include_lib("kernel/include/inet.hrl").
42
43%% calendar:system_time_to_universal_time/2
44-dialyzer({no_missing_calls, get_utc_datetime/1}).
45
46%%%=============================================================================
47%%% API
48%%%=============================================================================
49
50%%------------------------------------------------------------------------------
51%% @doc
52%% Returns the hostname of the running node. This may include the fully
53%% qualified domain name. The hostname will usually be the host part of the
54%% node name, except for the cases when the node is not alive or some strange
55%% host part was set, e.g. something related to the loopback interface. In this
56%% case the hostname will be what `inet:gethostname/0` returns, optionally with
57%% the domain removed.
58%% @end
59%%------------------------------------------------------------------------------
60-spec get_hostname(none | short | long) -> string().
61get_hostname(Transform) when is_atom(Transform) ->
62    get_hostname(Transform, get_hostpart(node())).
63get_hostname(none, HostPart) when is_list(HostPart) ->
64    clean_hostpart(HostPart);
65get_hostname(short, HostPart) when is_list(HostPart) ->
66    CleanHostPart = clean_hostpart(HostPart),
67    case is_ip4(CleanHostPart) of
68        true  -> CleanHostPart;
69        false -> hd(string:tokens(CleanHostPart, "."))
70    end;
71get_hostname(long, HostPart) when is_list(HostPart) ->
72    CleanHostPart = clean_hostpart(HostPart),
73    case is_ip4(CleanHostPart) of
74        true  -> CleanHostPart;
75        false ->
76            case lists:member($., CleanHostPart) of
77                true  -> CleanHostPart;
78                false ->
79                    case inet:gethostbyname(CleanHostPart) of
80                        {ok, #hostent{h_name=Hostname}} -> Hostname;
81                        {error, _}                      -> CleanHostPart
82                    end
83            end
84    end.
85
86%%------------------------------------------------------------------------------
87%% @doc
88%% Returns the name reported in the `APP-NAME' field. If no name is configured
89%% using the application environment, name part of the running node is returned.
90%% If the node is not running in distributed mode (no nodename configured) the
91%% string `"beam"' will be returned.
92%% @end
93%%------------------------------------------------------------------------------
94-spec get_name() -> binary().
95get_name() ->
96    case ?GET_ENV(app_name) of
97        {ok, Name} ->
98            to_type(binary, Name);
99        undefined ->
100            case ?GET_ENV(appname) of
101                {ok, Name} -> to_type(binary, Name);
102                undefined  -> get_name_from_node(node())
103            end
104    end.
105
106%%------------------------------------------------------------------------------
107%% @doc
108%% Returns the key that should be used to lookup a message metadata value to
109%% place in the `APP-NAME' field. If a message metadata field contains such a
110%% mapping this will have higher precendence over names configured/returned by
111%% {@link get_name/0}. `undefined' is used as the invalid/unconfigured value.
112%% @end
113%%------------------------------------------------------------------------------
114-spec get_name_metdata_key() -> atom() | undefined.
115get_name_metdata_key() ->
116    case ?GET_ENV(appname_from_metadata) of
117        {ok, Value} when is_atom(Value) -> Value;
118        _                               -> undefined
119    end.
120
121%%------------------------------------------------------------------------------
122%% @doc
123%% Returns the value for a specific key from the application environment. If no
124%% value is configured or the application environment can not be read the
125%% provided default value is returned.
126%% @end
127%%------------------------------------------------------------------------------
128-spec get_property(atom(), term()) -> term().
129get_property(Property, Default) ->
130    case {?GET_ENV(Property), Default} of
131        {{ok, Value}, _} -> Value;
132        {_, Value}       -> Value
133    end.
134
135%%------------------------------------------------------------------------------
136%% @doc
137%% Similar to {@link get_property/2}. Additionally, this function allows to
138%% specifiy the desired target type. The configured value will be converted to
139%% the desired type. If this is not possible, the function crashes.
140%% @end
141%%------------------------------------------------------------------------------
142-spec get_property(atom(), term(), binary | integer | ip_addr) -> term().
143get_property(Property, Default, Type) ->
144    to_type(Type, get_property(Property, Default)).
145
146%%------------------------------------------------------------------------------
147%% @doc
148%% Returns a string representation for a process. This will either be the
149%% (locally) registered name of the process or its process id.
150%% @end
151%%------------------------------------------------------------------------------
152-spec get_pid(pid() | atom() | string()) -> binary().
153get_pid(P) when is_pid(P) ->
154    case catch process_info(P, registered_name) of
155        {registered_name, N} -> to_type(binary, N);
156        _                    -> to_type(binary, P)
157    end;
158get_pid(N) ->
159    to_type(binary, N).
160
161%%------------------------------------------------------------------------------
162%% @doc
163%% Returns a syslog datetime object (UTC) with microsecond resolution from
164%% either an Erlang timestamp or the system time in microseconds.
165%% @end
166%%------------------------------------------------------------------------------
167-spec get_utc_datetime(erlang:timestamp() | pos_integer()) -> syslog:datetime().
168get_utc_datetime(SystemTime) when is_integer(SystemTime), SystemTime > 0 ->
169    MilliSecs = SystemTime div 1000,
170    MicroSecs = SystemTime rem 1000000,
171    Datetime = calendar:system_time_to_universal_time(MilliSecs, millisecond),
172    {Datetime, MicroSecs};
173get_utc_datetime({MegaSecs, Secs, MicroSecs}) ->
174    Datetime = calendar:now_to_universal_time({MegaSecs, Secs, 0}),
175    {Datetime, MicroSecs}.
176
177%%------------------------------------------------------------------------------
178%% @doc
179%% Returns the offset of a local datetime from the given UTC datetime.
180%% @end
181%%------------------------------------------------------------------------------
182-spec get_utc_offset(calendar:datetime(), calendar:datetime()) ->
183                            {43 | 45, 0..23, 0..59}.
184get_utc_offset(Utc, Local) when Utc < Local ->
185    {0, {H, Mi, 0}} = time_difference(Utc, Local),
186    {$+, H, Mi};
187get_utc_offset(Utc, Local) ->
188    {0, {H, Mi, 0}} = time_difference(Local, Utc),
189    {$-, H, Mi}.
190
191%%------------------------------------------------------------------------------
192%% @doc
193%% Returns a truncated string with at most `Len' characters/bytes.
194%% @end
195%%------------------------------------------------------------------------------
196-spec truncate(pos_integer(), string() | binary()) -> string() | binary().
197truncate(Len, Str) when length(Str) =< Len -> Str;
198truncate(Len, Str) when size(Str) =< Len   -> Str;
199truncate(Len, Str) when is_list(Str)       -> string:substr(Str, 1, Len);
200truncate(Len, Str) when is_binary(Str)     -> binary:part(Str, 0, Len).
201
202%%------------------------------------------------------------------------------
203%% @doc
204%% Formats a (UTC) timestamp according to RFC5424. The returned timestamp will
205%% be in local time with UTC offset (if available).
206%% @end
207%%------------------------------------------------------------------------------
208-spec format_rfc5424_date(syslog:datetime()) -> iodata().
209format_rfc5424_date({UtcDatetime, MicroSecs}) ->
210    LocaDatetime = erlang:universaltime_to_localtime(UtcDatetime),
211    format_rfc5424_date(LocaDatetime, UtcDatetime, MicroSecs).
212format_rfc5424_date(Utc = {{Y, Mo, D}, {H, Mi, S}}, Utc, Micro) ->
213    [integer_to_list(Y), $-, digit(Mo), $-, digit(D), $T,
214     digit(H), $:, digit(Mi), $:, digit(S), $., micro(Micro), $Z];
215format_rfc5424_date(Local = {{Y, Mo, D}, {H, Mi, S}}, Utc, Micro) ->
216    {Sign, OH, OMi} = get_utc_offset(Utc, Local),
217    [integer_to_list(Y), $-, digit(Mo), $-, digit(D), $T,
218     digit(H), $:, digit(Mi), $:, digit(S), $., micro(Micro),
219     Sign, digit(OH), $:, digit(OMi)].
220
221%%------------------------------------------------------------------------------
222%% @doc
223%% Formats a (UTC) timestamp according to RFC3164. The returned timestamp will
224%% be in local time.
225%% @end
226%%------------------------------------------------------------------------------
227-spec format_rfc3164_date(syslog:datetime()) -> iodata().
228format_rfc3164_date({UtcDatetime, _MicroSecs}) ->
229    format_rfc3164_date_(erlang:universaltime_to_localtime(UtcDatetime)).
230format_rfc3164_date_({{_, Mo, D}, {H, Mi, S}}) ->
231    [month(Mo), " ", day(D), " ", digit(H), $:, digit(Mi), $:, digit(S)].
232
233%%------------------------------------------------------------------------------
234%% @doc
235%% Returns structured data from a list or map of metadata (e.g. metadata
236%% provided in `lager' messages or `logger' events).
237%% @end
238%%------------------------------------------------------------------------------
239-spec get_structured_data(map() | list(), syslog:sd_id(), [atom()]) ->
240                                 [syslog:sd_element()].
241get_structured_data(Metadata, SDId, MDKeys) when is_map(Metadata) ->
242    get_structured_data(maps:to_list(Metadata), SDId, MDKeys);
243get_structured_data(Metadata, SDId, MDKeys) when is_list(Metadata) ->
244    case [D || D = {K, _} <- Metadata, lists:member(K, MDKeys)] of
245        []       -> [];
246        SDParams -> [{SDId, SDParams}]
247    end.
248
249%%------------------------------------------------------------------------------
250%% @doc
251%% Determine whether `error_logger' is available (e.g. we are running a pre
252%% OTP-21 release) or not.
253%% @end
254%%------------------------------------------------------------------------------
255-spec has_error_logger() -> boolean().
256has_error_logger() ->
257    case whereis(error_logger) of
258        P when is_pid(P) -> true;
259        undefined        -> false
260    end.
261
262%%------------------------------------------------------------------------------
263%% @doc
264%% Convert variables from one type to another
265%% @end
266%%------------------------------------------------------------------------------
267to_type(binary, V) when is_binary(V)   -> V;
268to_type(binary, V) when is_list(V)     -> list_to_binary(V);
269to_type(binary, V) when is_atom(V)     -> atom_to_binary(V, utf8);
270to_type(integer, V) when is_integer(V) -> V;
271to_type(integer, V) when is_list(V)    -> list_to_integer(V);
272to_type(ip_addr, V) when is_tuple(V)   -> V;
273to_type(ip_addr, V) when is_list(V)    -> to_ip_addr_type(V);
274to_type(Type, V) when is_pid(V)        -> to_type(Type, pid_to_list(V));
275to_type(Type, V) when is_integer(V)    -> to_type(Type, integer_to_list(V));
276to_type(Type, V) when is_binary(V)     -> to_type(Type, binary_to_list(V)).
277
278%%%=============================================================================
279%%% internal functions
280%%%=============================================================================
281
282%%------------------------------------------------------------------------------
283%% @private
284%%------------------------------------------------------------------------------
285get_name_from_node(Node) when is_atom(Node) ->
286    case atom_to_binary(Node, utf8) of
287        <<"nonode@nohost">> -> <<"beam">>;
288        N                   -> hd(binary:split(N, <<"@">>))
289    end.
290
291%%------------------------------------------------------------------------------
292%% @private
293%% Return the host part of the node name
294%%------------------------------------------------------------------------------
295get_hostpart(Node) when is_atom(Node) ->
296    lists:last(string:tokens(atom_to_list(Node), "@")).
297
298%%------------------------------------------------------------------------------
299%% @private
300%% Check for cases when the node is not alive or some strange host part was set,
301%% e.g. something related to the loopback interface and replace host part
302%% with the result of `inet:gethostname/0`
303%%------------------------------------------------------------------------------
304clean_hostpart("nohost") ->
305    element(2, inet:gethostname());
306clean_hostpart(HostPart) when is_list(HostPart) ->
307    case lists:member(HostPart, get_loopback_names()) of
308        true  -> element(2, inet:gethostname());
309        false -> HostPart
310    end.
311
312%%------------------------------------------------------------------------------
313%% @private
314%% Return all possible names of the available loopback interfaces, e.g.
315%% `["127.0.0.1", "localhost", "localhost.localdomain", ...]'
316%%------------------------------------------------------------------------------
317get_loopback_names() ->
318    Addresses = get_loopback_addresses(),
319    lists:append(
320      [Name || Addr <- Addresses,
321               Name <- [ntoa(Addr)],
322               is_list(Name)],
323      [Name || Addr <- Addresses,
324               {ok, Hostent} <- [inet:gethostbyaddr(Addr)],
325               Name <- [Hostent#hostent.h_name | Hostent#hostent.h_aliases],
326               is_list(Name)]).
327
328%%------------------------------------------------------------------------------
329%% @private
330%% Returns the IPv4 addresses associated with the available loopback interfaces.
331%%------------------------------------------------------------------------------
332get_loopback_addresses() ->
333    [Addr || {ok, IfList} <- [inet:getifaddrs()],
334             {_, IfProps} <- IfList,
335             {addr, Addr = {_, _, _, _}} <- IfProps,
336             {flags, IfFlags} <- IfProps,
337             lists:member(loopback, IfFlags)].
338
339%%------------------------------------------------------------------------------
340%% @private
341%%------------------------------------------------------------------------------
342ntoa(IPv4) -> lists:flatten(io_lib:format("~w.~w.~w.~w", tuple_to_list(IPv4))).
343
344%%------------------------------------------------------------------------------
345%% @private
346%%------------------------------------------------------------------------------
347is_ip4(Str) ->
348  re:run(Str, "\\A\\d+\\.\\d+\\.\\d+\\.\\d+\\Z", [{capture, none}]) =:= match.
349
350%%------------------------------------------------------------------------------
351%% @private
352%%------------------------------------------------------------------------------
353time_difference(T1, T2) ->
354    calendar:seconds_to_daystime(
355      calendar:datetime_to_gregorian_seconds(T2) -
356          calendar:datetime_to_gregorian_seconds(T1)).
357
358%%------------------------------------------------------------------------------
359%% @private
360%%------------------------------------------------------------------------------
361month(1)  -> "Jan";
362month(2)  -> "Feb";
363month(3)  -> "Mar";
364month(4)  -> "Apr";
365month(5)  -> "May";
366month(6)  -> "Jun";
367month(7)  -> "Jul";
368month(8)  -> "Aug";
369month(9)  -> "Sep";
370month(10) -> "Oct";
371month(11) -> "Nov";
372month(12) -> "Dec".
373
374%%------------------------------------------------------------------------------
375%% @private
376%%------------------------------------------------------------------------------
377day(1)  -> " 1";
378day(2)  -> " 2";
379day(3)  -> " 3";
380day(4)  -> " 4";
381day(5)  -> " 5";
382day(6)  -> " 6";
383day(7)  -> " 7";
384day(8)  -> " 8";
385day(9)  -> " 9";
386day(N)  -> integer_to_list(N).
387
388%%------------------------------------------------------------------------------
389%% @private
390%%------------------------------------------------------------------------------
391digit(0)  -> "00";
392digit(1)  -> "01";
393digit(2)  -> "02";
394digit(3)  -> "03";
395digit(4)  -> "04";
396digit(5)  -> "05";
397digit(6)  -> "06";
398digit(7)  -> "07";
399digit(8)  -> "08";
400digit(9)  -> "09";
401digit(N)  -> integer_to_list(N).
402
403%%------------------------------------------------------------------------------
404%% @private
405%%------------------------------------------------------------------------------
406micro(M) when M < 10     -> ["00000", integer_to_list(M)];
407micro(M) when M < 100    -> ["0000", integer_to_list(M)];
408micro(M) when M < 1000   -> ["000", integer_to_list(M)];
409micro(M) when M < 10000  -> ["00", integer_to_list(M)];
410micro(M) when M < 100000 -> ["0", integer_to_list(M)];
411micro(M)                 -> integer_to_list(M).
412
413%%------------------------------------------------------------------------------
414%% @private
415%%------------------------------------------------------------------------------
416to_ip_addr_type(V) ->
417    case inet:parse_address(V) of
418        {error, einval} -> try_inet_getaddr(V, [inet, inet6]);
419        {ok, IpAddr}    -> IpAddr
420    end.
421
422%%------------------------------------------------------------------------------
423%% @private
424%%------------------------------------------------------------------------------
425try_inet_getaddr(V, [AddrFamily | Rest]) ->
426    handle_inet_getaddr(inet:getaddr(V, AddrFamily), V, Rest).
427
428%%------------------------------------------------------------------------------
429%% @private
430%%------------------------------------------------------------------------------
431handle_inet_getaddr(_, _, []) ->
432    error(invalid_dest_host);
433handle_inet_getaddr({error, _}, V, AddrFamilies) ->
434    try_inet_getaddr(V, AddrFamilies);
435handle_inet_getaddr({ok, IpAddr}, _, _) ->
436    IpAddr.
437
438%%%=============================================================================
439%%% TESTS
440%%%=============================================================================
441
442-ifdef(TEST).
443
444-include_lib("eunit/include/eunit.hrl").
445
446get_hostname_ip_test() ->
447    ?assertEqual("192.168.1.1", get_hostname(none,  "192.168.1.1")),
448    ?assertEqual("192.168.1.1", get_hostname(short, "192.168.1.1")),
449    ?assertEqual("192.168.1.1", get_hostname(long,  "192.168.1.1")).
450
451get_hostname_none_test() ->
452    {ok, Hostname} = inet:gethostname(),
453    {ok, #hostent{h_name=Localhost}} = inet:gethostbyaddr("127.0.0.1"),
454    ?assertEqual(Hostname,          get_hostname(none, "nohost")),
455    ?assertEqual(Hostname,          get_hostname(none, "127.0.0.1")),
456    ?assertEqual(Hostname,          get_hostname(none, Localhost)),
457    ?assertEqual("hostname",        get_hostname(none, "hostname")),
458    ?assertEqual("hostname.domain", get_hostname(none, "hostname.domain")).
459
460get_hostname_short_test() ->
461    {ok, Hostname} = inet:gethostname(),
462    HostnameShort = hd(string:tokens(Hostname, ".")),
463    {ok, #hostent{h_name=Localhost}} = inet:gethostbyaddr("127.0.0.1"),
464    ?assertEqual(HostnameShort, get_hostname(short, "nohost")),
465    ?assertEqual(HostnameShort, get_hostname(short, "127.0.0.1")),
466    ?assertEqual(HostnameShort, get_hostname(short, Localhost)),
467    ?assertEqual("hostname",    get_hostname(short, "hostname")),
468    ?assertEqual("hostname",    get_hostname(short, "hostname.domain")).
469
470get_hostname_long_test() ->
471    {ok, Hostname} = inet:gethostname(),
472    HostnameLong1 = case inet:gethostbyname(Hostname) of
473        {ok, #hostent{h_name=Hname1}} -> Hname1;
474        {error, _}                    -> Hostname
475    end,
476    HostnameLong2 = case inet:gethostbyname("hostname") of
477        {ok, #hostent{h_name=Hname2}} -> Hname2;
478        {error, _}                    -> "hostname"
479    end,
480    {ok, #hostent{h_name=Localhost}} = inet:gethostbyaddr("127.0.0.1"),
481    ?assertEqual(HostnameLong1,     get_hostname(long, "nohost")),
482    ?assertEqual(HostnameLong1,     get_hostname(long, "127.0.0.1")),
483    ?assertEqual(HostnameLong1,     get_hostname(long, Localhost)),
484    ?assertEqual(HostnameLong2,     get_hostname(long, "hostname")),
485    ?assertEqual("hostname.domain", get_hostname(long, "hostname.domain")).
486
487get_name_from_node_test() ->
488    ?assertEqual(<<"beam">>,     get_name_from_node('nonode@nohost')),
489    ?assertEqual(<<"nodename">>, get_name_from_node('nodename@hostname')),
490    ?assertEqual(<<"nodename">>, get_name_from_node('nodename@hostname.dom.ain')).
491
492get_pid_test() ->
493    ?assertEqual(<<"init">>, get_pid(init)),
494    ?assertEqual(<<"init">>, get_pid(whereis(init))),
495    ?assertEqual(list_to_binary(pid_to_list(self())), get_pid(self())).
496
497truncate_test() ->
498    ?assertEqual("",        truncate(0, "")),
499    ?assertEqual("",        truncate(1, "")),
500    ?assertEqual("",        truncate(0, "123")),
501    ?assertEqual("1",       truncate(1, "123")),
502    ?assertEqual("12",      truncate(2, "123")),
503    ?assertEqual("123",     truncate(3, "123")),
504    ?assertEqual("123",     truncate(4, "123")),
505    ?assertEqual(<<>>,      truncate(0, <<>>)),
506    ?assertEqual(<<>>,      truncate(1, <<>>)),
507    ?assertEqual(<<>>,      truncate(0, <<"123">>)),
508    ?assertEqual(<<"1">>,   truncate(1, <<"123">>)),
509    ?assertEqual(<<"12">>,  truncate(2, <<"123">>)),
510    ?assertEqual(<<"123">>, truncate(3, <<"123">>)),
511    ?assertEqual(<<"123">>, truncate(4, <<"123">>)).
512
513format_rfc3164_date_test() ->
514    Datetime = {{{2013,4,6},{21,20,56}},908235},
515    Date = format_rfc3164_date(Datetime),
516    Rx = "Apr  [67] \\d\\d:20:56",
517    ?assertMatch({match, _}, re:run(lists:flatten(Date), Rx)).
518
519format_rfc5424_date_test() ->
520    Datetime = {{{2013,4,6},{21,20,56}},908235},
521    Date = format_rfc5424_date(Datetime),
522    Rx = "2013-04-0[67]T\\d\\d:\\d\\d:56\\.908235(Z|(\\+|-)\\d\\d:\\d\\d)",
523    ?assertMatch({match, _}, re:run(lists:flatten(Date), Rx)).
524
525to_type_test() ->
526    {ok, #hostent{h_name=Localhost}} = inet:gethostbyaddr("127.0.0.1"),
527    ?assertEqual(<<"1">>, to_type(binary, <<"1">>)),
528    ?assertEqual(<<"1">>, to_type(binary, "1")),
529    ?assertEqual(<<"1">>, to_type(binary, 1)),
530    ?assert(is_binary(to_type(binary, self()))),
531    ?assertEqual(<<"1">>, to_type(binary, '1')),
532    ?assertEqual(1, to_type(integer, <<"1">>)),
533    ?assertEqual(1, to_type(integer, "1")),
534    ?assertEqual(1, to_type(integer, 1)),
535    ?assertEqual({127,0,0,1}, to_type(ip_addr, Localhost)),
536    ?assertEqual({127,0,0,1}, to_type(ip_addr, list_to_binary(Localhost))),
537    ?assertEqual({127,0,0,1}, to_type(ip_addr, <<"127.0.0.1">>)),
538    ?assertEqual({127,0,0,1}, to_type(ip_addr, "127.0.0.1")),
539    ?assertEqual({0,0,0,0,0,0,0,1}, to_type(ip_addr, "::1")),
540    ?assertError(invalid_dest_host, to_type(ip_addr, "5dzFvraZ7lZUAlQu")).
541
542-endif. %% TEST
543