| 1 |
|
%%% eturnal STUN/TURN server. |
| 2 |
|
%%% |
| 3 |
|
%%% Copyright (c) 2020-2025 Holger Weiss <holger@zedat.fu-berlin.de>. |
| 4 |
|
%%% Copyright (c) 2020-2025 ProcessOne, SARL. |
| 5 |
|
%%% All rights reserved. |
| 6 |
|
%%% |
| 7 |
|
%%% Licensed under the Apache License, Version 2.0 (the "License"); |
| 8 |
|
%%% you may not use this file except in compliance with the License. |
| 9 |
|
%%% You may obtain a copy of the License at |
| 10 |
|
%%% |
| 11 |
|
%%% http://www.apache.org/licenses/LICENSE-2.0 |
| 12 |
|
%%% |
| 13 |
|
%%% Unless required by applicable law or agreed to in writing, software |
| 14 |
|
%%% distributed under the License is distributed on an "AS IS" BASIS, |
| 15 |
|
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 16 |
|
%%% See the License for the specific language governing permissions and |
| 17 |
|
%%% limitations under the License. |
| 18 |
|
|
| 19 |
|
-module(eturnal_ctl). |
| 20 |
|
-export([get_credentials/2, |
| 21 |
|
get_password/1, |
| 22 |
|
get_sessions/0, |
| 23 |
|
get_sessions/1, |
| 24 |
|
get_status/0, |
| 25 |
|
get_info/0, |
| 26 |
|
get_version/0, |
| 27 |
|
get_loglevel/0, |
| 28 |
|
set_loglevel/1, |
| 29 |
|
disconnect/1, |
| 30 |
|
reload/0]). |
| 31 |
|
|
| 32 |
|
-include_lib("kernel/include/logger.hrl"). |
| 33 |
|
-include("eturnal.hrl"). |
| 34 |
|
|
| 35 |
|
-type sock_mod() :: gen_udp | gen_tcp | fast_tls. |
| 36 |
|
-type addr() :: inet:ip_address(). |
| 37 |
|
-type addr_port() :: {inet:ip_address(), inet:port_number()}. |
| 38 |
|
|
| 39 |
|
-record(session, |
| 40 |
|
{pid :: pid(), |
| 41 |
|
sid :: binary(), |
| 42 |
|
user :: binary(), |
| 43 |
|
sock_mod :: sock_mod(), |
| 44 |
|
client_addr :: addr_port(), |
| 45 |
|
relay_addr :: addr_port(), |
| 46 |
|
perm_addrs :: [addr()], |
| 47 |
|
peer_addrs :: [addr_port()], |
| 48 |
|
sent_bytes :: non_neg_integer(), |
| 49 |
|
sent_pkts :: non_neg_integer(), |
| 50 |
|
rcvd_bytes :: non_neg_integer(), |
| 51 |
|
rcvd_pkts :: non_neg_integer(), |
| 52 |
|
start_time :: integer()}). |
| 53 |
|
|
| 54 |
|
-type session() :: #session{}. |
| 55 |
|
|
| 56 |
|
%% API. |
| 57 |
|
|
| 58 |
|
-spec get_credentials(term(), term()) -> {ok, string()} | {error, string()}. |
| 59 |
|
get_credentials(Expiry, Suffix) -> |
| 60 |
10 |
?LOG_DEBUG("Handling API call: get_credentials(~p, ~p)", [Expiry, Suffix]), |
| 61 |
10 |
try make_username(Expiry, Suffix) of |
| 62 |
|
Username -> |
| 63 |
6 |
case call({get_password, Username}) of |
| 64 |
|
{ok, Password} -> |
| 65 |
6 |
Credentials = format_credentials(Username, Password), |
| 66 |
6 |
{ok, unicode:characters_to_list(Credentials)}; |
| 67 |
|
{error, no_credentials} -> |
| 68 |
:-( |
{error, "No shared secret and no credentials"}; |
| 69 |
|
{error, timeout} -> |
| 70 |
:-( |
{error, "Querying eturnal timed out"} |
| 71 |
|
end |
| 72 |
|
catch _:badarg -> |
| 73 |
4 |
?LOG_DEBUG("Invalid argument(s): ~p:~p", [Expiry, Suffix]), |
| 74 |
4 |
{error, "Invalid expiry or suffix"} |
| 75 |
|
end. |
| 76 |
|
|
| 77 |
|
-spec get_password(term()) -> {ok, string()} | {error, string()}. |
| 78 |
|
get_password(Username0) -> |
| 79 |
9 |
?LOG_DEBUG("Handling API call: get_password(~p)", [Username0]), |
| 80 |
9 |
try unicode:characters_to_binary(Username0) of |
| 81 |
|
Username when is_binary(Username) -> |
| 82 |
7 |
case call({get_password, Username}) of |
| 83 |
|
{ok, Password} -> |
| 84 |
6 |
{ok, unicode:characters_to_list(Password)}; |
| 85 |
|
{error, no_credentials} -> |
| 86 |
1 |
{error, "No shared secret and no credentials"}; |
| 87 |
|
{error, timeout} -> |
| 88 |
:-( |
{error, "Querying eturnal timed out"} |
| 89 |
|
end; |
| 90 |
|
{_, _, _} -> |
| 91 |
:-( |
?LOG_DEBUG("Cannot convert user name to binary: ~p", [Username0]), |
| 92 |
:-( |
{error, "User name must be specified as a string"} |
| 93 |
|
catch _:badarg -> |
| 94 |
2 |
?LOG_DEBUG("Cannot convert user name to binary: ~p", [Username0]), |
| 95 |
2 |
{error, "User name must be specified as a string"} |
| 96 |
|
end. |
| 97 |
|
|
| 98 |
|
-spec get_sessions() -> {ok, string()} | {error, string()}. |
| 99 |
|
get_sessions() -> |
| 100 |
1 |
?LOG_DEBUG("Handling API call: get_sessions()"), |
| 101 |
1 |
case query_all_sessions() of |
| 102 |
|
[_ | _] = Sessions -> |
| 103 |
:-( |
{ok, unicode:characters_to_list(format_sessions(Sessions))}; |
| 104 |
|
[] -> |
| 105 |
1 |
{ok, "No active TURN sessions"} |
| 106 |
|
end. |
| 107 |
|
|
| 108 |
|
-spec get_sessions(term()) -> {ok, string()} | {error, string()}. |
| 109 |
|
get_sessions(Username0) -> |
| 110 |
3 |
?LOG_DEBUG("Handling API call: get_sessions(~p)", [Username0]), |
| 111 |
3 |
try unicode:characters_to_binary(Username0) of |
| 112 |
|
Username when is_binary(Username) -> |
| 113 |
2 |
case query_user_sessions(Username) of |
| 114 |
|
[_ | _] = Sessions -> |
| 115 |
:-( |
{ok, unicode:characters_to_list(format_sessions(Sessions))}; |
| 116 |
|
[] -> |
| 117 |
2 |
{ok, "No active TURN sessions"} |
| 118 |
|
end; |
| 119 |
|
{_, _, _} -> |
| 120 |
:-( |
?LOG_DEBUG("Cannot convert user name to binary: ~p", [Username0]), |
| 121 |
:-( |
{error, "User name must be specified as a string"} |
| 122 |
|
catch _:badarg -> |
| 123 |
1 |
?LOG_DEBUG("Cannot convert user name to binary: ~p", [Username0]), |
| 124 |
1 |
{error, "User name must be specified as a string"} |
| 125 |
|
end. |
| 126 |
|
|
| 127 |
|
-spec get_status() -> {ok, string()} | {error, string()}. |
| 128 |
|
get_status() -> |
| 129 |
1 |
?LOG_DEBUG("Handling API call: get_status()"), |
| 130 |
1 |
case call(get_status) of |
| 131 |
|
ok -> |
| 132 |
1 |
{ok, "eturnal is running"}; |
| 133 |
|
{error, timeout} -> |
| 134 |
:-( |
{error, "Querying eturnal timed out"} |
| 135 |
|
end. |
| 136 |
|
|
| 137 |
|
-spec get_info() -> {ok, string()} | {error, string()}. |
| 138 |
|
get_info() -> |
| 139 |
1 |
?LOG_DEBUG("Handling API call: get_info()"), |
| 140 |
1 |
case call(get_info) of |
| 141 |
|
{ok, Info} -> |
| 142 |
1 |
{ok, unicode:characters_to_list(format_info(Info))}; |
| 143 |
|
{error, timeout} -> |
| 144 |
:-( |
{error, "Querying eturnal timed out"} |
| 145 |
|
end. |
| 146 |
|
|
| 147 |
|
-spec get_version() -> {ok, string()} | {error, string()}. |
| 148 |
|
get_version() -> |
| 149 |
1 |
?LOG_DEBUG("Handling API call: get_version()"), |
| 150 |
1 |
case call(get_version) of |
| 151 |
|
{ok, Version} -> |
| 152 |
1 |
{ok, unicode:characters_to_list(Version)}; |
| 153 |
|
{error, timeout} -> |
| 154 |
:-( |
{error, "Querying eturnal timed out"} |
| 155 |
|
end. |
| 156 |
|
|
| 157 |
|
-spec get_loglevel() -> {ok, string()} | {error, string()}. |
| 158 |
|
get_loglevel() -> |
| 159 |
2 |
?LOG_DEBUG("Handling API call: get_loglevel()"), |
| 160 |
2 |
case call(get_loglevel) of |
| 161 |
|
{ok, Level} -> |
| 162 |
2 |
{ok, atom_to_list(Level)}; |
| 163 |
|
{error, timeout} -> |
| 164 |
:-( |
{error, "Querying eturnal timed out"} |
| 165 |
|
end. |
| 166 |
|
|
| 167 |
|
-spec set_loglevel(term()) -> ok | {error, string()}. |
| 168 |
|
set_loglevel(Level) when is_atom(Level) -> |
| 169 |
2 |
?LOG_DEBUG("Handling API call: set_loglevel(~s)", [Level]), |
| 170 |
2 |
case eturnal_logger:is_valid_level(Level) of |
| 171 |
|
true -> |
| 172 |
1 |
case call({set_loglevel, Level}) of |
| 173 |
|
ok -> |
| 174 |
1 |
ok; |
| 175 |
|
{error, timeout} -> |
| 176 |
:-( |
{error, "Querying eturnal timed out"} |
| 177 |
|
end; |
| 178 |
|
false -> |
| 179 |
1 |
?LOG_DEBUG("Invalid log level: ~s", [Level]), |
| 180 |
1 |
{error, "Not a valid log level: " ++ atom_to_list(Level)} |
| 181 |
|
end; |
| 182 |
|
set_loglevel(Level) -> |
| 183 |
:-( |
?LOG_DEBUG("Invalid API call: set_loglevel(~p)", [Level]), |
| 184 |
:-( |
{error, "Log level must be specified as an atom"}. |
| 185 |
|
|
| 186 |
|
-spec disconnect(term()) -> {ok, string()} | {error, string()}. |
| 187 |
|
disconnect(Username0) -> |
| 188 |
2 |
?LOG_DEBUG("Handling API call: disconnect(~p)", [Username0]), |
| 189 |
2 |
try unicode:characters_to_binary(Username0) of |
| 190 |
|
Username when is_binary(Username) -> |
| 191 |
1 |
N = disconnect_user(Username), |
| 192 |
1 |
Msg = io_lib:format("Disconnected ~B TURN session(s)", [N]), |
| 193 |
1 |
{ok, unicode:characters_to_list(Msg)}; |
| 194 |
|
{_, _, _} -> |
| 195 |
:-( |
?LOG_DEBUG("Cannot convert user name to binary: ~p", [Username0]), |
| 196 |
:-( |
{error, "User name must be specified as a string"} |
| 197 |
|
catch _:badarg -> |
| 198 |
1 |
?LOG_DEBUG("Cannot convert user name to binary: ~p", [Username0]), |
| 199 |
1 |
{error, "User name must be specified as a string"} |
| 200 |
|
end. |
| 201 |
|
|
| 202 |
|
-spec reload() -> ok | {error, string()}. |
| 203 |
|
reload() -> |
| 204 |
2 |
?LOG_DEBUG("Handling API call: reload()"), |
| 205 |
2 |
case call(reload) of |
| 206 |
|
ok -> |
| 207 |
2 |
ok; |
| 208 |
|
{error, timeout} -> |
| 209 |
:-( |
{error, "Querying eturnal timed out"}; |
| 210 |
|
{error, Reason} -> |
| 211 |
:-( |
{error, conf:format_error(Reason)} |
| 212 |
|
end. |
| 213 |
|
|
| 214 |
|
%% Internal functions. |
| 215 |
|
|
| 216 |
|
-spec make_username(string(), string()) -> binary(). |
| 217 |
|
make_username(Expiry0, Suffix) -> |
| 218 |
10 |
Expiry = try string:trim(Expiry0) of |
| 219 |
|
Trimmed -> |
| 220 |
7 |
Trimmed |
| 221 |
|
catch _:function_clause -> |
| 222 |
3 |
erlang:error(badarg) |
| 223 |
|
end, |
| 224 |
7 |
try calendar:rfc3339_to_system_time(Expiry) of |
| 225 |
|
Time -> |
| 226 |
1 |
username_from_timestamp(Time, Suffix) |
| 227 |
|
catch |
| 228 |
|
_:function_clause -> |
| 229 |
6 |
username_from_expiry(Expiry, Suffix); |
| 230 |
|
_:{badmatch, _} -> % Erlang/OTP < 28.0. |
| 231 |
:-( |
username_from_expiry(Expiry, Suffix); |
| 232 |
|
_:badarg -> % Erlang/OTP < 21.3. |
| 233 |
:-( |
username_from_expiry(Expiry, Suffix) |
| 234 |
|
end. |
| 235 |
|
|
| 236 |
|
-spec username_from_timestamp(integer(), string()) -> binary(). |
| 237 |
|
username_from_timestamp(Time, []) -> |
| 238 |
1 |
integer_to_binary(Time); |
| 239 |
|
username_from_timestamp(Time, Suffix) -> |
| 240 |
5 |
Username = io_lib:format("~B:~s", [Time, Suffix]), |
| 241 |
5 |
unicode:characters_to_binary(Username). |
| 242 |
|
|
| 243 |
|
-spec username_from_expiry(string(), string()) -> binary(). |
| 244 |
|
username_from_expiry(Expiry0, Suffix) -> |
| 245 |
6 |
case {unicode:characters_to_binary(Expiry0), |
| 246 |
|
io_lib:printable_unicode_list(Suffix)} of |
| 247 |
|
{Expiry, true} when is_binary(Expiry) -> |
| 248 |
6 |
Time = erlang:system_time(second) + parse_expiry(Expiry), |
| 249 |
5 |
username_from_timestamp(Time, Suffix); |
| 250 |
|
{_, _} -> |
| 251 |
:-( |
erlang:error(badarg) |
| 252 |
|
end. |
| 253 |
|
|
| 254 |
|
-spec parse_expiry(binary()) -> pos_integer(). |
| 255 |
|
parse_expiry(Expiry) -> |
| 256 |
6 |
case string:to_integer(Expiry) of |
| 257 |
|
{N, <<>>} when is_integer(N), N > 0 -> |
| 258 |
1 |
N; |
| 259 |
|
{N, <<"s">>} when is_integer(N), N > 0 -> |
| 260 |
1 |
N; |
| 261 |
|
{N, <<"m">>} when is_integer(N), N > 0 -> |
| 262 |
1 |
N * 60; |
| 263 |
|
{N, <<"h">>} when is_integer(N), N > 0 -> |
| 264 |
1 |
N * 3600; |
| 265 |
|
{N, <<"d">>} when is_integer(N), N > 0 -> |
| 266 |
1 |
N * 86400; |
| 267 |
|
{_, _} -> |
| 268 |
1 |
erlang:error(badarg) |
| 269 |
|
end. |
| 270 |
|
|
| 271 |
|
-spec filter_sessions(fun((session()) -> boolean())) -> [session()]. |
| 272 |
|
filter_sessions(Pred) -> |
| 273 |
4 |
lists:filtermap( |
| 274 |
|
fun({_, PID, worker, _}) -> |
| 275 |
:-( |
try query_state(PID) of |
| 276 |
|
State -> |
| 277 |
:-( |
Session = #session{ |
| 278 |
|
pid = PID, |
| 279 |
|
sid = element(27, State), |
| 280 |
|
user = element(6, State), |
| 281 |
|
sock_mod = element(2, State), |
| 282 |
|
client_addr = element(4, State), |
| 283 |
|
relay_addr = element(18, State), |
| 284 |
|
perm_addrs = maps:keys(element(12, State)), |
| 285 |
|
peer_addrs = maps:keys(element(10, State)), |
| 286 |
|
sent_bytes = element(32, State), |
| 287 |
|
sent_pkts = element(33, State), |
| 288 |
|
rcvd_bytes = element(30, State), |
| 289 |
|
rcvd_pkts = element(31, State), |
| 290 |
|
start_time = element(34, State)}, |
| 291 |
:-( |
case Pred(Session) of |
| 292 |
|
true -> |
| 293 |
:-( |
{true, Session}; |
| 294 |
|
false -> |
| 295 |
:-( |
false |
| 296 |
|
end |
| 297 |
|
catch exit:{Reason, _} when Reason =:= noproc; |
| 298 |
|
Reason =:= normal; |
| 299 |
|
Reason =:= shutdown; |
| 300 |
|
Reason =:= killed; |
| 301 |
|
Reason =:= timeout -> |
| 302 |
:-( |
?LOG_DEBUG("Cannot query TURN session ~p: ~s", |
| 303 |
:-( |
[PID, Reason]), |
| 304 |
:-( |
false |
| 305 |
|
end |
| 306 |
|
end, supervisor:which_children(turn_tmp_sup)). |
| 307 |
|
|
| 308 |
|
-spec query_user_sessions(binary()) -> [session()]. |
| 309 |
|
query_user_sessions(Username) -> |
| 310 |
3 |
Pred = fun(#session{user = User}) when User =:= Username -> |
| 311 |
:-( |
true; |
| 312 |
|
(#session{user = User}) -> % Match 1256900400:Username. |
| 313 |
:-( |
case binary:split(User, <<$:>>) of |
| 314 |
|
[_Expiration, Username] -> |
| 315 |
:-( |
true; |
| 316 |
|
_ -> |
| 317 |
:-( |
false |
| 318 |
|
end |
| 319 |
|
end, |
| 320 |
3 |
filter_sessions(Pred). |
| 321 |
|
|
| 322 |
|
-spec query_all_sessions() -> [session()]. |
| 323 |
|
query_all_sessions() -> |
| 324 |
1 |
filter_sessions(fun(_Session) -> true end). |
| 325 |
|
|
| 326 |
|
-spec query_state(pid()) -> tuple(). |
| 327 |
|
query_state(PID) -> % Until we add a proper API to 'stun'. |
| 328 |
:-( |
{value, State} = lists:search( |
| 329 |
|
fun(E) -> |
| 330 |
:-( |
is_tuple(E) andalso element(1, E) =:= state |
| 331 |
|
end, sys:get_state(PID)), |
| 332 |
:-( |
State. |
| 333 |
|
|
| 334 |
|
-spec disconnect_user(binary()) -> non_neg_integer(). |
| 335 |
|
disconnect_user(User) -> |
| 336 |
1 |
lists:foldl(fun(#session{user = U, pid = PID, sid = SID}, N) -> |
| 337 |
:-( |
?LOG_DEBUG("Disconnecting session ~s of ~s", [SID, U]), |
| 338 |
:-( |
_ = supervisor:terminate_child(turn_tmp_sup, PID), |
| 339 |
:-( |
N + 1 |
| 340 |
|
end, 0, query_user_sessions(User)). |
| 341 |
|
|
| 342 |
|
-spec format_sessions([session()]) -> io_lib:chars(). |
| 343 |
|
format_sessions(Sessions) -> |
| 344 |
:-( |
H = io_lib:format("~B active TURN sessions:", [length(Sessions)]), |
| 345 |
:-( |
T = lists:map( |
| 346 |
|
fun(#session{user = User, |
| 347 |
|
sock_mod = SockMod, |
| 348 |
|
client_addr = ClientAddr, |
| 349 |
|
relay_addr = RelayAddr, |
| 350 |
|
perm_addrs = PermAddrs, |
| 351 |
|
peer_addrs = PeerAddrs, |
| 352 |
|
sent_bytes = SentBytes, |
| 353 |
|
sent_pkts = SentPkts, |
| 354 |
|
rcvd_bytes = RcvdBytes, |
| 355 |
|
rcvd_pkts = RcvdPkts, |
| 356 |
|
start_time = StartTime}) -> |
| 357 |
:-( |
Duration0 = erlang:monotonic_time() - StartTime, |
| 358 |
:-( |
Duration = erlang:convert_time_unit( |
| 359 |
|
Duration0, native, second), |
| 360 |
:-( |
Transport = format_transport(SockMod), |
| 361 |
:-( |
Client = eturnal_misc:addr_to_str(ClientAddr), |
| 362 |
:-( |
Relay = eturnal_misc:addr_to_str(RelayAddr), |
| 363 |
:-( |
Peers = format_addrs(PeerAddrs), |
| 364 |
:-( |
Perms = format_addrs(PermAddrs), |
| 365 |
:-( |
io_lib:format( |
| 366 |
|
"-- TURN session of ~ts --~s" |
| 367 |
|
" Client: ~s (~s)~s" |
| 368 |
|
" Relay: ~s (UDP)~s" |
| 369 |
|
" Permission(s): ~s~s" |
| 370 |
|
" Peer(s): ~s~s" |
| 371 |
|
" Sent: ~B KiB (~B packets)~s" |
| 372 |
|
" Received: ~B KiB (~B packets)~s" |
| 373 |
|
" Running for: ~B seconds", |
| 374 |
|
[User, nl(), |
| 375 |
|
Client, Transport, nl(), |
| 376 |
|
Relay, nl(), |
| 377 |
|
Perms, nl(), |
| 378 |
|
Peers, nl(), |
| 379 |
|
round(SentBytes / 1024), SentPkts, nl(), |
| 380 |
|
round(RcvdBytes / 1024), RcvdPkts, nl(), |
| 381 |
|
Duration]) |
| 382 |
|
end, Sessions), |
| 383 |
:-( |
lists:join([nl(), nl()], [H | T]). |
| 384 |
|
|
| 385 |
|
-spec format_info(eturnal_node_info()) -> io_lib:chars(). |
| 386 |
|
format_info(#eturnal_node_info{ |
| 387 |
|
eturnal_vsn = EturnalVsn, |
| 388 |
|
otp_vsn = {OtpVsn, ErtsVsn}, |
| 389 |
|
uptime = Uptime, |
| 390 |
|
num_sessions = Sessions, |
| 391 |
|
num_processes = Procs, |
| 392 |
|
num_reductions = Reductions, |
| 393 |
|
total_queue_len = QueueLen, |
| 394 |
|
total_memory = Memory}) -> |
| 395 |
1 |
MiB = round(Memory / 1024 / 1024), |
| 396 |
1 |
Seconds = erlang:convert_time_unit(Uptime, millisecond, second), |
| 397 |
1 |
{Ds, {Hs, Ms, Ss}} = calendar:seconds_to_daystime(Seconds), |
| 398 |
1 |
io_lib:format( |
| 399 |
|
"eturnal ~s on Erlang/OTP ~s (ERTS ~s)~s" |
| 400 |
|
"Uptime: ~B days, ~B hours, ~B minutes, ~B seconds~s" |
| 401 |
|
"Active TURN sessions: ~B~s" |
| 402 |
|
"Processes: ~B~s" |
| 403 |
|
"Total length of run queues: ~B~s" |
| 404 |
|
"Total CPU usage (reductions): ~B~s" |
| 405 |
|
"Allocated memory (MiB): ~B", |
| 406 |
|
[EturnalVsn, OtpVsn, ErtsVsn, nl(), |
| 407 |
|
Ds, Hs, Ms, Ss, nl(), |
| 408 |
|
Sessions, nl(), |
| 409 |
|
Procs, nl(), |
| 410 |
|
QueueLen, nl(), |
| 411 |
|
Reductions, nl(), |
| 412 |
|
MiB]). |
| 413 |
|
|
| 414 |
|
-spec format_transport(sock_mod()) -> binary(). |
| 415 |
|
format_transport(gen_udp) -> |
| 416 |
:-( |
<<"UDP">>; |
| 417 |
|
format_transport(gen_tcp) -> |
| 418 |
:-( |
<<"TCP">>; |
| 419 |
|
format_transport(fast_tls) -> |
| 420 |
:-( |
<<"TLS">>. |
| 421 |
|
|
| 422 |
|
-spec format_addrs([addr() | addr_port()]) -> iodata(). |
| 423 |
|
format_addrs([]) -> |
| 424 |
:-( |
<<"none">>; |
| 425 |
|
format_addrs(PeerAddrs) -> |
| 426 |
:-( |
[lists:join(", ", lists:map(fun eturnal_misc:addr_to_str/1, PeerAddrs)), |
| 427 |
|
<<" (UDP)">>]. |
| 428 |
|
|
| 429 |
|
-spec format_credentials(binary(), binary()) -> iodata(). |
| 430 |
|
format_credentials(Username, Password) -> |
| 431 |
6 |
io_lib:format("Username: ~s~s" |
| 432 |
|
"Password: ~s", |
| 433 |
|
[Username, nl(), |
| 434 |
|
Password]). |
| 435 |
|
|
| 436 |
|
-spec nl() -> string(). |
| 437 |
|
nl() -> |
| 438 |
12 |
[$~, $n]. % Let the caller convert "~n"s to actual newline characters. |
| 439 |
|
|
| 440 |
|
-spec call(term()) -> ok | {ok | error, term()}. |
| 441 |
|
call(Request) -> |
| 442 |
21 |
try gen_server:call(eturnal, Request, timer:minutes(1)) of |
| 443 |
|
ok -> |
| 444 |
4 |
?LOG_DEBUG("eturnal call (~p) returned ok", [Request]), |
| 445 |
4 |
ok; |
| 446 |
|
{ok, _Value} = Result -> |
| 447 |
16 |
?LOG_DEBUG("eturnal call (~p) returned ~p", [Request, Result]), |
| 448 |
16 |
Result; |
| 449 |
|
{error, _Reason} = Err -> |
| 450 |
1 |
?LOG_DEBUG("eturnal call (~p) returned ~p", [Request, Err]), |
| 451 |
1 |
Err |
| 452 |
|
catch exit:{timeout, _} -> |
| 453 |
:-( |
?LOG_DEBUG("eturnal call (~p) timed out", [Request]), |
| 454 |
:-( |
{error, timeout} |
| 455 |
|
end. |