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