/home/runner/work/eturnal/eturnal/_build/test/cover/ct/eturnal_ctl.html

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