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

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.
Line Hits Source