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

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