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

1 %%% eturnal STUN/TURN server.
2 %%%
3 %%% Copyright (c) 2020-2023 Holger Weiss <holger@zedat.fu-berlin.de>.
4 %%% Copyright (c) 2020-2023 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 1 ?LOG_DEBUG("Handling API call: get_sessions(~p)", [Username0]),
111 1 try unicode:characters_to_binary(Username0) of
112 Username when is_binary(Username) ->
113 1 case query_user_sessions(Username) of
114 [_ | _] = Sessions ->
115
:-(
{ok, unicode:characters_to_list(format_sessions(Sessions))};
116 [] ->
117 1 {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
:-(
?LOG_DEBUG("Cannot convert user name to binary: ~p", [Username0]),
124
:-(
{error, "User name must be specified as a string"}
125 end.
126
127 -spec get_status() -> {ok, string()} | {error, string()}.
128 get_status() ->
129
:-(
?LOG_DEBUG("Handling API call: get_status()"),
130
:-(
case call(get_status) of
131 ok ->
132
:-(
{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 3 ?LOG_DEBUG("Handling API call: disconnect(~p)", [Username0]),
189 3 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 2 ?LOG_DEBUG("Cannot convert user name to binary: ~p", [Username0]),
199 2 {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 _:{badmatch, _} ->
229 6 username_from_expiry(Expiry, Suffix);
230 _:badarg -> % Erlang/OTP < 21.3.
231
:-(
username_from_expiry(Expiry, Suffix)
232 end.
233
234 -spec username_from_timestamp(integer(), string()) -> binary().
235 username_from_timestamp(Time, []) ->
236 1 integer_to_binary(Time);
237 username_from_timestamp(Time, Suffix) ->
238 5 Username = io_lib:format("~B:~s", [Time, Suffix]),
239 5 unicode:characters_to_binary(Username).
240
241 -spec username_from_expiry(string(), string()) -> binary().
242 username_from_expiry(Expiry0, Suffix) ->
243 6 case {unicode:characters_to_binary(Expiry0),
244 io_lib:printable_unicode_list(Suffix)} of
245 {Expiry, true} when is_binary(Expiry) ->
246 6 Time = erlang:system_time(second) + parse_expiry(Expiry),
247 5 username_from_timestamp(Time, Suffix);
248 {_, _} ->
249
:-(
erlang:error(badarg)
250 end.
251
252 -spec parse_expiry(binary()) -> pos_integer().
253 parse_expiry(Expiry) ->
254 6 case string:to_integer(Expiry) of
255 {N, <<>>} when is_integer(N), N > 0 ->
256 1 N;
257 {N, <<"s">>} when is_integer(N), N > 0 ->
258 1 N;
259 {N, <<"m">>} when is_integer(N), N > 0 ->
260 1 N * 60;
261 {N, <<"h">>} when is_integer(N), N > 0 ->
262 1 N * 3600;
263 {N, <<"d">>} when is_integer(N), N > 0 ->
264 1 N * 86400;
265 {_, _} ->
266 1 erlang:error(badarg)
267 end.
268
269 -spec filter_sessions(fun((session()) -> boolean())) -> [session()].
270 filter_sessions(Pred) ->
271 3 lists:filtermap(
272 fun({_, PID, worker, _}) ->
273
:-(
try query_state(PID) of
274 State ->
275
:-(
Session = #session{
276 pid = PID,
277 sid = element(27, State),
278 user = element(6, State),
279 sock_mod = element(2, State),
280 client_addr = element(4, State),
281 relay_addr = element(18, State),
282 perm_addrs = maps:keys(element(12, State)),
283 peer_addrs = maps:keys(element(10, State)),
284 sent_bytes = element(32, State),
285 sent_pkts = element(33, State),
286 rcvd_bytes = element(30, State),
287 rcvd_pkts = element(31, State),
288 start_time = element(34, State)},
289
:-(
case Pred(Session) of
290 true ->
291
:-(
{true, Session};
292 false ->
293
:-(
false
294 end
295 catch exit:{Reason, _} when Reason =:= noproc;
296 Reason =:= normal;
297 Reason =:= shutdown;
298 Reason =:= killed;
299 Reason =:= timeout ->
300
:-(
?LOG_DEBUG("Cannot query TURN session ~p: ~s",
301
:-(
[PID, Reason]),
302
:-(
false
303 end
304 end, supervisor:which_children(turn_tmp_sup)).
305
306 -spec query_user_sessions(binary()) -> [session()].
307 query_user_sessions(Username) ->
308 2 Pred = fun(#session{user = User}) when User =:= Username ->
309
:-(
true;
310 (#session{user = User}) -> % Match 1256900400:Username.
311
:-(
case binary:split(User, <<$:>>) of
312 [_Expiration, Username] ->
313
:-(
true;
314 _ ->
315
:-(
false
316 end
317 end,
318 2 filter_sessions(Pred).
319
320 -spec query_all_sessions() -> [session()].
321 query_all_sessions() ->
322 1 filter_sessions(fun(_Session) -> true end).
323
324 -spec query_state(pid()) -> tuple().
325 query_state(PID) -> % Until we add a proper API to 'stun'.
326
:-(
{value, State} = lists:search(
327 fun(E) ->
328
:-(
is_tuple(E) andalso element(1, E) =:= state
329 end, sys:get_state(PID)),
330
:-(
State.
331
332 -spec disconnect_user(binary()) -> non_neg_integer().
333 disconnect_user(User) ->
334 1 lists:foldl(fun(#session{user = U, pid = PID, sid = SID}, N) ->
335
:-(
?LOG_DEBUG("Disconnecting session ~s of ~s", [SID, U]),
336
:-(
_ = supervisor:terminate_child(turn_tmp_sup, PID),
337
:-(
N + 1
338 end, 0, query_user_sessions(User)).
339
340 -spec format_sessions([session()]) -> io_lib:chars().
341 format_sessions(Sessions) ->
342
:-(
H = io_lib:format("~B active TURN sessions:", [length(Sessions)]),
343
:-(
T = lists:map(
344 fun(#session{user = User,
345 sock_mod = SockMod,
346 client_addr = ClientAddr,
347 relay_addr = RelayAddr,
348 perm_addrs = PermAddrs,
349 peer_addrs = PeerAddrs,
350 sent_bytes = SentBytes,
351 sent_pkts = SentPkts,
352 rcvd_bytes = RcvdBytes,
353 rcvd_pkts = RcvdPkts,
354 start_time = StartTime}) ->
355
:-(
Duration0 = erlang:monotonic_time() - StartTime,
356
:-(
Duration = erlang:convert_time_unit(
357 Duration0, native, second),
358
:-(
Transport = format_transport(SockMod),
359
:-(
Client = eturnal_misc:addr_to_str(ClientAddr),
360
:-(
Relay = eturnal_misc:addr_to_str(RelayAddr),
361
:-(
Peers = format_addrs(PeerAddrs),
362
:-(
Perms = format_addrs(PermAddrs),
363
:-(
io_lib:format(
364 "-- TURN session of ~ts --~s"
365 " Client: ~s (~s)~s"
366 " Relay: ~s (UDP)~s"
367 " Permission(s): ~s~s"
368 " Peer(s): ~s~s"
369 " Sent: ~B KiB (~B packets)~s"
370 " Received: ~B KiB (~B packets)~s"
371 " Running for: ~B seconds",
372 [User, nl(),
373 Client, Transport, nl(),
374 Relay, nl(),
375 Perms, nl(),
376 Peers, nl(),
377 round(SentBytes / 1024), SentPkts, nl(),
378 round(RcvdBytes / 1024), RcvdPkts, nl(),
379 Duration])
380 end, Sessions),
381
:-(
lists:join([nl(), nl()], [H | T]).
382
383 -spec format_info(eturnal_node_info()) -> io_lib:chars().
384 format_info(#eturnal_node_info{
385 eturnal_vsn = EturnalVsn,
386 otp_vsn = {OtpVsn, ErtsVsn},
387 uptime = Uptime,
388 num_sessions = Sessions,
389 num_processes = Procs,
390 num_reductions = Reductions,
391 total_queue_len = QueueLen,
392 total_memory = Memory}) ->
393 1 MiB = round(Memory / 1024 / 1024),
394 1 Seconds = erlang:convert_time_unit(Uptime, millisecond, second),
395 1 {Ds, {Hs, Ms, Ss}} = calendar:seconds_to_daystime(Seconds),
396 1 io_lib:format(
397 "eturnal ~s on Erlang/OTP ~s (ERTS ~s)~s"
398 "Uptime: ~B days, ~B hours, ~B minutes, ~B seconds~s"
399 "Active TURN sessions: ~B~s"
400 "Processes: ~B~s"
401 "Total length of run queues: ~B~s"
402 "Total CPU usage (reductions): ~B~s"
403 "Allocated memory (MiB): ~B",
404 [EturnalVsn, OtpVsn, ErtsVsn, nl(),
405 Ds, Hs, Ms, Ss, nl(),
406 Sessions, nl(),
407 Procs, nl(),
408 QueueLen, nl(),
409 Reductions, nl(),
410 MiB]).
411
412 -spec format_transport(sock_mod()) -> binary().
413 format_transport(gen_udp) ->
414
:-(
<<"UDP">>;
415 format_transport(gen_tcp) ->
416
:-(
<<"TCP">>;
417 format_transport(fast_tls) ->
418
:-(
<<"TLS">>.
419
420 -spec format_addrs([addr() | addr_port()]) -> iodata().
421 format_addrs([]) ->
422
:-(
<<"none">>;
423 format_addrs(PeerAddrs) ->
424
:-(
[lists:join(", ", lists:map(fun eturnal_misc:addr_to_str/1, PeerAddrs)),
425 <<" (UDP)">>].
426
427 -spec format_credentials(binary(), binary()) -> iodata().
428 format_credentials(Username, Password) ->
429 6 io_lib:format("Username: ~s~s"
430 "Password: ~s",
431 [Username, nl(),
432 Password]).
433
434 -spec nl() -> string().
435 nl() ->
436 12 [$~, $n]. % Let the caller convert "~n"s to actual newline characters.
437
438 -spec call(term()) -> ok | {ok | error, term()}.
439 call(Request) ->
440 20 try gen_server:call(eturnal, Request, timer:minutes(1)) of
441 ok ->
442 3 ?LOG_DEBUG("eturnal call (~p) returned ok", [Request]),
443 3 ok;
444 {ok, _Value} = Result ->
445 16 ?LOG_DEBUG("eturnal call (~p) returned ~p", [Request, Result]),
446 16 Result;
447 {error, _Reason} = Err ->
448 1 ?LOG_DEBUG("eturnal call (~p) returned ~p", [Request, Err]),
449 1 Err
450 catch exit:{timeout, _} ->
451
:-(
?LOG_DEBUG("eturnal call (~p) timed out", [Request]),
452
:-(
{error, timeout}
453 end.
Line Hits Source