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 |
:-( |
?LOG_DEBUG("Handling API call: get_credentials(~p, ~p)", [Expiry, Suffix]), |
61 |
:-( |
try make_username(Expiry, Suffix) of |
62 |
|
Username -> |
63 |
:-( |
case call({get_password, Username}) of |
64 |
|
{ok, Password} -> |
65 |
:-( |
Credentials = format_credentials(Username, Password), |
66 |
:-( |
{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 |
:-( |
?LOG_DEBUG("Invalid argument(s): ~p:~p", [Expiry, Suffix]), |
74 |
:-( |
{error, "Invalid expiry or suffix"} |
75 |
|
end. |
76 |
|
|
77 |
|
-spec get_password(term()) -> {ok, string()} | {error, string()}. |
78 |
|
get_password(Username0) -> |
79 |
:-( |
?LOG_DEBUG("Handling API call: get_password(~p)", [Username0]), |
80 |
:-( |
try unicode:characters_to_binary(Username0) of |
81 |
|
Username when is_binary(Username) -> |
82 |
:-( |
case call({get_password, Username}) of |
83 |
|
{ok, Password} -> |
84 |
:-( |
{ok, unicode:characters_to_list(Password)}; |
85 |
|
{error, no_credentials} -> |
86 |
:-( |
{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 |
:-( |
?LOG_DEBUG("Cannot convert user name to binary: ~p", [Username0]), |
95 |
:-( |
{error, "User name must be specified as a string"} |
96 |
|
end. |
97 |
|
|
98 |
|
-spec get_sessions() -> {ok, string()} | {error, string()}. |
99 |
|
get_sessions() -> |
100 |
:-( |
?LOG_DEBUG("Handling API call: get_sessions()"), |
101 |
:-( |
case query_all_sessions() of |
102 |
|
[_ | _] = Sessions -> |
103 |
:-( |
{ok, unicode:characters_to_list(format_sessions(Sessions))}; |
104 |
|
[] -> |
105 |
:-( |
{ok, "No active TURN sessions"} |
106 |
|
end. |
107 |
|
|
108 |
|
-spec get_sessions(term()) -> {ok, string()} | {error, string()}. |
109 |
|
get_sessions(Username0) -> |
110 |
:-( |
?LOG_DEBUG("Handling API call: get_sessions(~p)", [Username0]), |
111 |
:-( |
try unicode:characters_to_binary(Username0) of |
112 |
|
Username when is_binary(Username) -> |
113 |
:-( |
case query_user_sessions(Username) of |
114 |
|
[_ | _] = Sessions -> |
115 |
:-( |
{ok, unicode:characters_to_list(format_sessions(Sessions))}; |
116 |
|
[] -> |
117 |
:-( |
{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 |
:-( |
?LOG_DEBUG("Handling API call: get_info()"), |
140 |
:-( |
case call(get_info) of |
141 |
|
{ok, Info} -> |
142 |
:-( |
{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 |
:-( |
?LOG_DEBUG("Handling API call: get_version()"), |
150 |
:-( |
case call(get_version) of |
151 |
|
{ok, Version} -> |
152 |
:-( |
{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 |
:-( |
?LOG_DEBUG("Handling API call: get_loglevel()"), |
160 |
:-( |
case call(get_loglevel) of |
161 |
|
{ok, Level} -> |
162 |
:-( |
{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 |
:-( |
?LOG_DEBUG("Handling API call: set_loglevel(~s)", [Level]), |
170 |
:-( |
case eturnal_logger:is_valid_level(Level) of |
171 |
|
true -> |
172 |
:-( |
case call({set_loglevel, Level}) of |
173 |
|
ok -> |
174 |
:-( |
ok; |
175 |
|
{error, timeout} -> |
176 |
:-( |
{error, "Querying eturnal timed out"} |
177 |
|
end; |
178 |
|
false -> |
179 |
:-( |
?LOG_DEBUG("Invalid log level: ~s", [Level]), |
180 |
:-( |
{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 |
:-( |
?LOG_DEBUG("Handling API call: disconnect(~p)", [Username0]), |
189 |
:-( |
try unicode:characters_to_binary(Username0) of |
190 |
|
Username when is_binary(Username) -> |
191 |
:-( |
N = disconnect_user(Username), |
192 |
:-( |
Msg = io_lib:format("Disconnected ~B TURN session(s)", [N]), |
193 |
:-( |
{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 |
:-( |
?LOG_DEBUG("Cannot convert user name to binary: ~p", [Username0]), |
199 |
:-( |
{error, "User name must be specified as a string"} |
200 |
|
end. |
201 |
|
|
202 |
|
-spec reload() -> ok | {error, string()}. |
203 |
|
reload() -> |
204 |
:-( |
?LOG_DEBUG("Handling API call: reload()"), |
205 |
:-( |
case call(reload) of |
206 |
|
ok -> |
207 |
:-( |
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 |
:-( |
Expiry = try string:trim(Expiry0) of |
219 |
|
Trimmed -> |
220 |
:-( |
Trimmed |
221 |
|
catch _:function_clause -> |
222 |
:-( |
erlang:error(badarg) |
223 |
|
end, |
224 |
:-( |
try calendar:rfc3339_to_system_time(Expiry) of |
225 |
|
Time -> |
226 |
:-( |
username_from_timestamp(Time, Suffix) |
227 |
|
catch |
228 |
|
_:{badmatch, _} -> |
229 |
:-( |
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 |
:-( |
integer_to_binary(Time); |
237 |
|
username_from_timestamp(Time, Suffix) -> |
238 |
:-( |
Username = io_lib:format("~B:~s", [Time, Suffix]), |
239 |
:-( |
unicode:characters_to_binary(Username). |
240 |
|
|
241 |
|
-spec username_from_expiry(string(), string()) -> binary(). |
242 |
|
username_from_expiry(Expiry0, Suffix) -> |
243 |
:-( |
case {unicode:characters_to_binary(Expiry0), |
244 |
|
io_lib:printable_unicode_list(Suffix)} of |
245 |
|
{Expiry, true} when is_binary(Expiry) -> |
246 |
:-( |
Time = erlang:system_time(second) + parse_expiry(Expiry), |
247 |
:-( |
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 |
:-( |
case string:to_integer(Expiry) of |
255 |
|
{N, <<>>} when is_integer(N), N > 0 -> |
256 |
:-( |
N; |
257 |
|
{N, <<"s">>} when is_integer(N), N > 0 -> |
258 |
:-( |
N; |
259 |
|
{N, <<"m">>} when is_integer(N), N > 0 -> |
260 |
:-( |
N * 60; |
261 |
|
{N, <<"h">>} when is_integer(N), N > 0 -> |
262 |
:-( |
N * 3600; |
263 |
|
{N, <<"d">>} when is_integer(N), N > 0 -> |
264 |
:-( |
N * 86400; |
265 |
|
{_, _} -> |
266 |
:-( |
erlang:error(badarg) |
267 |
|
end. |
268 |
|
|
269 |
|
-spec filter_sessions(fun((session()) -> boolean())) -> [session()]. |
270 |
|
filter_sessions(Pred) -> |
271 |
:-( |
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 |
:-( |
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 |
:-( |
filter_sessions(Pred). |
319 |
|
|
320 |
|
-spec query_all_sessions() -> [session()]. |
321 |
|
query_all_sessions() -> |
322 |
:-( |
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 |
:-( |
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 |
:-( |
MiB = round(Memory / 1024 / 1024), |
394 |
:-( |
Seconds = erlang:convert_time_unit(Uptime, millisecond, second), |
395 |
:-( |
{Ds, {Hs, Ms, Ss}} = calendar:seconds_to_daystime(Seconds), |
396 |
:-( |
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 |
:-( |
io_lib:format("Username: ~s~s" |
430 |
|
"Password: ~s", |
431 |
|
[Username, nl(), |
432 |
|
Password]). |
433 |
|
|
434 |
|
-spec nl() -> string(). |
435 |
|
nl() -> |
436 |
:-( |
[$~, $n]. % Let the caller convert "~n"s to actual newline characters. |
437 |
|
|
438 |
|
-spec call(term()) -> ok | {ok | error, term()}. |
439 |
|
call(Request) -> |
440 |
:-( |
try gen_server:call(eturnal, Request, timer:minutes(1)) of |
441 |
|
ok -> |
442 |
:-( |
?LOG_DEBUG("eturnal call (~p) returned ok", [Request]), |
443 |
:-( |
ok; |
444 |
|
{ok, _Value} = Result -> |
445 |
:-( |
?LOG_DEBUG("eturnal call (~p) returned ~p", [Request, Result]), |
446 |
:-( |
Result; |
447 |
|
{error, _Reason} = Err -> |
448 |
:-( |
?LOG_DEBUG("eturnal call (~p) returned ~p", [Request, Err]), |
449 |
:-( |
Err |
450 |
|
catch exit:{timeout, _} -> |
451 |
:-( |
?LOG_DEBUG("eturnal call (~p) timed out", [Request]), |
452 |
:-( |
{error, timeout} |
453 |
|
end. |