/home/runner/work/eturnal/eturnal/_build/test/cover/eunit/eturnal.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).
20 -behaviour(gen_server).
21 -export([start/0,
22 stop/0]).
23 -export([start_link/0]).
24 -export([init/1,
25 handle_call/3,
26 handle_cast/2,
27 handle_info/2,
28 terminate/2,
29 code_change/3]).
30 -export([init_config/0,
31 config_is_loaded/0,
32 run_hook/2,
33 get_password/2,
34 get_opt/1,
35 create_self_signed/1,
36 reload/3,
37 abort/1]).
38 -export_type([transport/0,
39 option/0,
40 value/0,
41 config_changes/0,
42 state/0]).
43
44 -ifdef(EUNIT).
45 -include_lib("eunit/include/eunit.hrl").
46 -endif.
47 -include_lib("kernel/include/logger.hrl").
48 -define(PEM_FILE_NAME, "cert.pem").
49
50 -record(eturnal_state,
51 {listeners :: listeners(),
52 modules :: modules()}).
53
54 -type transport() :: udp | tcp | tls | auto.
55 -type option() :: atom().
56 -type value() :: term().
57 -type config_changes() :: {[{option(), value()}],
58 [{option(), value()}],
59 [option()]}.
60
61 -opaque state() :: #eturnal_state{}.
62
63 -type listeners() :: [{inet:ip_address(), inet:port_number(), transport()}].
64 -type modules() :: [module()].
65
66 %% API: non-release startup and shutdown (used by test suite).
67
68 -spec start() -> ok | {error, term()}.
69 start() ->
70
:-(
case application:ensure_all_started(eturnal) of
71 {ok, _Started} ->
72
:-(
ok;
73 {error, _Reason} = Err ->
74
:-(
Err
75 end.
76
77 -spec stop() -> ok | {error, term()}.
78 stop() ->
79
:-(
application:stop(eturnal).
80
81 %% API: supervisor callback.
82
83 -spec start_link() -> {ok, pid()} | ignore | {error, term()}.
84 start_link() ->
85
:-(
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
86
87 %% API: gen_server callbacks.
88
89 -spec init(any()) -> {ok, state()}.
90 init(_Opts) ->
91
:-(
process_flag(trap_exit, true),
92
:-(
ok = eturnal_module:init(),
93
:-(
ok = log_relay_addresses(),
94
:-(
ok = log_control_listener(),
95
:-(
try
96
:-(
ok = ensure_run_dir(),
97
:-(
ok = check_turn_config(),
98
:-(
ok = check_proxy_config(),
99
:-(
_R = check_pem_file()
100 catch exit:Reason1 ->
101
:-(
abort(Reason1)
102 end,
103
:-(
try {start_modules(), start_listeners()} of
104 {Modules, Listeners} ->
105
:-(
?LOG_DEBUG("Started ~B modules", [length(Modules)]),
106
:-(
?LOG_DEBUG("Started ~B listeners", [length(Listeners)]),
107
:-(
{ok, #eturnal_state{listeners = Listeners, modules = Modules}}
108 catch exit:Reason2 ->
109
:-(
abort(Reason2)
110 end.
111
112 -spec handle_call(reload | get_info | get_version | get_loglevel |
113 {set_loglevel, eturnal_logger:level()} |
114 {get_password, binary()} | term(),
115 {pid(), term()}, state())
116 -> {reply, ok | {ok, term()} | {error, term()}, state()}.
117 handle_call(reload, _From, State) ->
118
:-(
case reload(State) of
119 {ok, State1} ->
120
:-(
{reply, ok, State1};
121 {error, _Reason} = Err ->
122
:-(
{reply, Err, State}
123 end;
124 handle_call(get_info, _From, State) ->
125
:-(
Info = eturnal_misc:info(),
126
:-(
{reply, {ok, Info}, State};
127 handle_call(get_version, _From, State) ->
128
:-(
Version = eturnal_misc:version(),
129
:-(
{reply, {ok, Version}, State};
130 handle_call(get_loglevel, _From, State) ->
131
:-(
Level = eturnal_logger:get_level(),
132
:-(
{reply, {ok, Level}, State};
133 handle_call({set_loglevel, Level}, _From, State) ->
134
:-(
try
135
:-(
ok = eturnal_logger:set_level(Level),
136
:-(
{reply, ok, State}
137 catch error:{badmatch, {error, _Reason} = Err} ->
138
:-(
{reply, Err, State}
139 end;
140 handle_call({get_password, Username}, _From, State) ->
141
:-(
case {get_opt(secret), is_dynamic_username(Username)} of
142 {[Secret | _Secrets], true} ->
143
:-(
Password = derive_password(Username, [Secret]),
144
:-(
{reply, {ok, Password}, State};
145 {_, _} ->
146
:-(
case maps:get(Username, get_opt(credentials), undefined) of
147 Password when is_binary(Password) ->
148
:-(
{reply, {ok, Password}, State};
149 undefined ->
150
:-(
{reply, {error, no_credentials}, State}
151 end
152 end;
153 handle_call(Request, From, State) ->
154
:-(
?LOG_ERROR("Got unexpected request from ~p: ~p", [From, Request]),
155
:-(
{reply, {error, badarg}, State}.
156
157 -spec handle_cast(reload |
158 {config_change, config_changes(),
159 fun(() -> ok), fun(() -> ok)} | term(), state())
160 -> {noreply, state()}.
161 handle_cast(reload, State) ->
162
:-(
case reload(State) of
163 {ok, State1} ->
164
:-(
{noreply, State1};
165 {error, _Reason} ->
166
:-(
{noreply, State}
167 end;
168 handle_cast({config_change, Changes, BeginFun, EndFun}, State) ->
169
:-(
ok = BeginFun(),
170
:-(
State1 = apply_config_changes(State, Changes),
171
:-(
ok = EndFun(),
172
:-(
{noreply, State1};
173 handle_cast(Msg, State) ->
174
:-(
?LOG_ERROR("Got unexpected message: ~p", [Msg]),
175
:-(
{noreply, State}.
176
177 -spec handle_info(term(), state()) -> {noreply, state()}.
178 handle_info(Info, State) ->
179
:-(
?LOG_ERROR("Got unexpected info: ~p", [Info]),
180
:-(
{noreply, State}.
181
182 -spec terminate(normal | shutdown | {shutdown, term()} | term(), state()) -> ok.
183 terminate(Reason, State) ->
184
:-(
?LOG_DEBUG("Terminating ~s (~p)", [?MODULE, Reason]),
185
:-(
try stop_listeners(State)
186 catch exit:Reason1 ->
187
:-(
?LOG_ERROR(format_error(Reason1))
188 end,
189
:-(
try stop_modules(State)
190 catch exit:Reason2 ->
191
:-(
?LOG_ERROR(format_error(Reason2))
192 end,
193
:-(
try clean_run_dir()
194 catch exit:Reason3 ->
195
:-(
?LOG_ERROR(format_error(Reason3))
196 end,
197
:-(
_ = eturnal_module:terminate(),
198
:-(
ok.
199
200 -spec code_change({down, term()} | term(), state(), term()) -> {ok, state()}.
201 code_change(_OldVsn, State, _Extra) ->
202
:-(
?LOG_NOTICE("Upgraded to eturnal ~s, reapplying configuration",
203
:-(
[eturnal_misc:version()]),
204
:-(
ok = reload_config(),
205
:-(
{ok, State}.
206
207 %% API: (re)load configuration.
208
209 -spec init_config() -> ok.
210 init_config() -> % Just to cope with an empty configuration file.
211
:-(
case config_is_loaded() of
212 true ->
213
:-(
?LOG_DEBUG("Configuration has been loaded successfully"),
214
:-(
ok;
215 false ->
216
:-(
?LOG_DEBUG("Empty configuration, using defaults"),
217
:-(
ok = conf:load([{eturnal, []}])
218 end.
219
220 -spec config_is_loaded() -> boolean().
221 config_is_loaded() ->
222
:-(
try eturnal:get_opt(realm) of
223 Realm when is_binary(Realm) ->
224
:-(
true
225 catch error:{badmatch, undefined} ->
226
:-(
false
227 end.
228
229 %% API: stun callbacks.
230
231 -spec run_hook(eturnal_module:event(), eturnal_module:info()) -> ok.
232 run_hook(Event, Info) ->
233
:-(
eturnal_module:handle_event(Event, Info).
234
235 -spec get_password(binary(), binary())
236 -> binary() | [binary()] | {expired, binary() | [binary()]}.
237 get_password(Username, _Realm) ->
238
:-(
[Expiration | _Suffix] = binary:split(Username, <<$:>>),
239
:-(
try binary_to_integer(Expiration) of
240 ExpireTime ->
241
:-(
case erlang:system_time(second) of
242 Now when Now < ExpireTime ->
243
:-(
?LOG_DEBUG("Deriving password for: ~ts", [Username]),
244
:-(
derive_password(Username, get_opt(secret));
245 Now when Now >= ExpireTime ->
246
:-(
case get_opt(strict_expiry) of
247 true ->
248
:-(
?LOG_INFO("Credentials expired: ~ts", [Username]),
249
:-(
<<>>;
250 false ->
251
:-(
?LOG_DEBUG("Credentials expired: ~ts", [Username]),
252
:-(
{expired,
253 derive_password(Username, get_opt(secret))}
254 end
255 end
256 catch _:badarg ->
257
:-(
?LOG_DEBUG("Looking up password for: ~ts", [Username]),
258
:-(
case maps:get(Username, get_opt(credentials), undefined) of
259 Password when is_binary(Password) ->
260
:-(
Password;
261 undefined ->
262
:-(
?LOG_INFO("Have no password for: ~ts", [Username]),
263
:-(
<<>>
264 end
265 end.
266
267 %% API: retrieve option value.
268
269 -spec get_opt(option()) -> value().
270 get_opt(Opt) ->
271
:-(
{ok, Val} = application:get_env(eturnal, Opt),
272
:-(
Val.
273
274 %% API: create self-signed certificate.
275
276 -spec create_self_signed(file:filename_all()) -> ok.
277 create_self_signed(File) ->
278
:-(
try
279
:-(
PEM = eturnal_cert:create(get_opt(realm)),
280
:-(
ok = touch(File),
281
:-(
ok = file:write_file(File, PEM, [raw])
282 catch error:{_, {error, Reason}} ->
283
:-(
exit({pem_failure, File, Reason})
284 end.
285
286 %% API: reload service.
287
288 -spec reload(config_changes(), fun(() -> ok), fun(() -> ok)) -> ok.
289 reload(ConfigChanges, BeginFun, EndFun) ->
290
:-(
Msg = {config_change, ConfigChanges, BeginFun, EndFun},
291
:-(
ok = gen_server:cast(?MODULE, Msg).
292
293 %% API: abnormal termination.
294
295 -spec abort(term()) -> no_return().
296 abort(Reason) ->
297
:-(
case application:get_env(eturnal, on_fail, halt) of
298 exit ->
299
:-(
?LOG_CRITICAL("Stopping: ~s", [format_error(Reason)]),
300
:-(
exit(Reason);
301 _Halt ->
302
:-(
?LOG_CRITICAL("Aborting: ~s", [format_error(Reason)]),
303
:-(
eturnal_logger:flush(),
304
:-(
halt(1)
305 end.
306
307 %% Internal functions: reload configuration.
308
309 -spec reload_config() -> ok.
310 reload_config() ->
311
:-(
ok = gen_server:cast(?MODULE, reload).
312
313 %% Internal functions: authentication.
314
315 -spec is_dynamic_username(binary()) -> boolean().
316 is_dynamic_username(Username) ->
317
:-(
case string:to_integer(Username) of
318 {N, <<":", _Rest/binary>>} when is_integer(N), N > 0 ->
319
:-(
true;
320 {N, <<>>} when is_integer(N), N > 0 ->
321
:-(
true;
322 {_, _} ->
323
:-(
false
324 end.
325
326 -spec derive_password(binary(), [binary()]) -> binary() | [binary()].
327 -ifdef(old_crypto).
328 derive_password(Username, [Secret]) ->
329 base64:encode(crypto:hmac(sha, Secret, Username));
330 derive_password(Username, Secrets) when is_list(Secrets) ->
331 [derive_password(Username, [Secret]) || Secret <- Secrets].
332 -else.
333 derive_password(Username, [Secret]) ->
334
:-(
base64:encode(crypto:mac(hmac, sha, Secret, Username));
335 derive_password(Username, Secrets) when is_list(Secrets) ->
336
:-(
[derive_password(Username, [Secret]) || Secret <- Secrets].
337 -endif.
338
339 %% Internal functions: log relay address(es) and distribution listener port.
340
341 -spec log_relay_addresses() -> ok.
342 log_relay_addresses() ->
343
:-(
Min = get_opt(relay_min_port),
344
:-(
Max = get_opt(relay_max_port),
345
:-(
case get_opt(relay_ipv4_addr) of
346 {_, _, _, _} = Addr4 ->
347
:-(
?LOG_INFO("Relay IPv4 address: ~s (port range: ~B-~B)",
348
:-(
[inet:ntoa(Addr4), Min, Max]);
349 undefined ->
350
:-(
?LOG_INFO("Relay IPv4 address not configured")
351 end,
352
:-(
case get_opt(relay_ipv6_addr) of
353 {_, _, _, _, _, _, _, _} = Addr6 ->
354
:-(
?LOG_INFO("Relay IPv6 address: ~s (port range: ~B-~B)",
355
:-(
[inet:ntoa(Addr6), Min, Max]);
356 undefined ->
357
:-(
?LOG_INFO("Relay IPv6 address not configured")
358 end.
359
360 -spec log_control_listener() -> ok.
361 -dialyzer({[no_fail_call, no_match], log_control_listener/0}). % OTP 21/22.
362 log_control_listener() ->
363
:-(
[Name, Host] = string:split(atom_to_list(node()), "@"),
364 % The 'catch' calms Dialyzer on OTP 21 (even though we don't match 'EXIT').
365
:-(
case catch erl_epmd:port_please(Name, Host, timer:seconds(10)) of
366 {port, Port, Version} ->
367
:-(
?LOG_INFO("Listening on ~s:~B (tcp) (Erlang protocol version ~B)",
368
:-(
[Host, Port, Version]);
369 {error, Reason} ->
370
:-(
?LOG_INFO("Cannot determine control query port: ~p", [Reason]);
371 Reason when is_atom(Reason) ->
372
:-(
?LOG_INFO("Cannot determine control query port: ~s", [Reason])
373 end.
374
375 %% Internal functions: module startup/shutdown.
376
377 -spec start_modules() -> modules().
378 start_modules() ->
379
:-(
lists:map(
380 fun({Mod, _Opts}) ->
381
:-(
case eturnal_module:start(Mod) of
382 ok ->
383
:-(
?LOG_INFO("Started ~s", [Mod]),
384
:-(
Mod;
385 {error, Reason} ->
386
:-(
exit({module_failure, start, Mod, Reason})
387 end
388 end, maps:to_list(get_opt(modules))).
389
390 -spec stop_modules(state()) -> ok.
391 stop_modules(#eturnal_state{modules = Modules}) ->
392
:-(
lists:foreach(
393 fun(Mod) ->
394
:-(
case eturnal_module:stop(Mod) of
395 ok ->
396
:-(
?LOG_INFO("Stopped ~s", [Mod]);
397 {error, Reason} ->
398
:-(
exit({module_failure, stop, Mod, Reason})
399 end
400 end, Modules).
401
402 %% Internal functions: listener startup/shutdown.
403
404 -spec start_listeners() -> listeners().
405 start_listeners() ->
406
:-(
Opts = lists:filtermap(
407 fun({InKey, OutKey}) ->
408
:-(
opt_filter({OutKey, get_opt(InKey)})
409 end, opt_map()) ++ [{auth_fun, fun ?MODULE:get_password/2},
410 {hook_fun, fun ?MODULE:run_hook/2}]
411 ++ blacklist_opts()
412 ++ whitelist_opts(),
413
:-(
lists:map(
414 fun({IP, Port, Transport, ProxyProtocol, EnableTURN}) ->
415
:-(
Opts1 = tls_opts(Transport) ++ Opts,
416
:-(
Opts2 = turn_opts(EnableTURN) ++ Opts1,
417
:-(
Opts3 = proxy_opts(ProxyProtocol) ++ Opts2,
418
:-(
?LOG_DEBUG("Starting listener ~s (~s) with options:~n~p",
419 [eturnal_misc:addr_to_str(IP, Port), Transport,
420
:-(
Opts3]),
421
:-(
InfoArgs = [eturnal_misc:addr_to_str(IP, Port), Transport,
422 describe_listener(EnableTURN)],
423
:-(
case stun_listener:add_listener(IP, Port, Transport, Opts3) of
424 ok ->
425
:-(
?LOG_INFO("Listening on ~s (~s) (~s)", InfoArgs);
426 {error, already_started} ->
427
:-(
?LOG_INFO("Already listening on ~s (~s) (~s)", InfoArgs);
428 {error, Reason} ->
429
:-(
exit({listener_failure, start, IP, Port, Transport,
430 Reason})
431 end,
432
:-(
{IP, Port, Transport}
433 end, get_opt(listen)).
434
435 -spec stop_listeners(state()) -> ok.
436 stop_listeners(#eturnal_state{listeners = Listeners}) ->
437
:-(
lists:foreach(
438 fun({IP, Port, Transport}) ->
439
:-(
case stun_listener:del_listener(IP, Port, Transport) of
440 ok ->
441
:-(
?LOG_INFO("Stopped listening on ~s (~s)",
442 [eturnal_misc:addr_to_str(IP, Port),
443
:-(
Transport]);
444 {error, Reason} ->
445
:-(
exit({listener_failure, stop, IP, Port, Transport,
446 Reason})
447 end
448 end, Listeners).
449
450 -spec describe_listener(boolean()) -> binary().
451 describe_listener(true = _EnableTURN) ->
452
:-(
<<"STUN/TURN">>;
453 describe_listener(false = _EnableTURN) ->
454
:-(
<<"STUN only">>.
455
456 -spec opt_map() -> [{atom(), atom()}].
457 opt_map() ->
458
:-(
[{relay_ipv4_addr, turn_ipv4_address},
459 {relay_ipv6_addr, turn_ipv6_address},
460 {relay_min_port, turn_min_port},
461 {relay_max_port, turn_max_port},
462 {max_allocations, turn_max_allocations},
463 {max_permissions, turn_max_permissions},
464 {max_bps, shaper},
465 {realm, auth_realm},
466 {software_name, server_name}].
467
468 -spec opt_filter(Opt) -> {true, Opt} | false when Opt :: {option(), value()}.
469 opt_filter({turn_ipv6_address, undefined}) ->
470
:-(
false; % The 'stun' application currently wouldn't accept 'undefined'.
471 opt_filter(Opt) ->
472
:-(
{true, Opt}.
473
474 -spec turn_opts(boolean()) -> proplists:proplist().
475 turn_opts(EnableTURN) ->
476
:-(
case {EnableTURN, got_credentials(), got_relay_addr()} of
477 {true, true, true} ->
478
:-(
[{use_turn, true},
479 {auth_type, user}];
480 {_, _, _} ->
481
:-(
[{use_turn, false},
482 {auth_type, anonymous}]
483 end.
484
485 -spec proxy_opts(boolean()) -> proplists:proplist().
486 proxy_opts(true = _ProxyProtocol) ->
487
:-(
[proxy_protocol];
488 proxy_opts(false = _ProxyProtocol) ->
489
:-(
[].
490
491 %% This function can be removed in favor of opt_map/0 entries once the
492 %% 'blacklist' option is removed.
493 -spec blacklist_opts() -> proplists:proplist().
494 blacklist_opts() ->
495
:-(
case {eturnal:get_opt(blacklist),
496 eturnal:get_opt(blacklist_clients),
497 eturnal:get_opt(blacklist_peers)} of
498 {[], Clients, Peers} ->
499
:-(
[{turn_blacklist_clients, Clients},
500 {turn_blacklist_peers, Peers}];
501 {Blacklist, Clients, Peers} ->
502
:-(
?LOG_WARNING("The 'blacklist' option is deprecated"),
503
:-(
?LOG_WARNING("Use 'blacklist_clients' and/or 'blacklist_peers'"),
504
:-(
[{turn_blacklist_clients, lists:usort(Clients ++ Blacklist)},
505 {turn_blacklist_peers, lists:usort(Peers ++ Blacklist)}]
506 end.
507
508 %% This function can be removed in favor of opt_map/0 entries once the
509 %% 'whitelist' option is removed.
510 -spec whitelist_opts() -> proplists:proplist().
511 whitelist_opts() ->
512
:-(
case {eturnal:get_opt(whitelist),
513 eturnal:get_opt(whitelist_clients),
514 eturnal:get_opt(whitelist_peers)} of
515 {[], Clients, Peers} ->
516
:-(
[{turn_whitelist_clients, Clients},
517 {turn_whitelist_peers, Peers}];
518 {Whitelist, Clients, Peers} ->
519
:-(
?LOG_WARNING("The 'whitelist' option is deprecated"),
520
:-(
?LOG_WARNING("Use 'whitelist_clients' and/or 'whitelist_peers'"),
521
:-(
[{turn_whitelist_clients, lists:usort(Clients ++ Whitelist)},
522 {turn_whitelist_peers, lists:usort(Peers ++ Whitelist)}]
523 end.
524
525 -spec tls_opts(transport()) -> proplists:proplist().
526 -ifdef(old_inet_backend).
527 tls_opts(tls) ->
528 [{tls, true} | extra_tls_opts()];
529 tls_opts(auto) ->
530 exit({otp_too_old, transport, auto, 23});
531 tls_opts(_) ->
532 [].
533 -else.
534 tls_opts(tls) ->
535
:-(
[{tls, true} | extra_tls_opts()];
536 tls_opts(auto) ->
537
:-(
[{tls, optional} | extra_tls_opts()];
538 tls_opts(_) ->
539
:-(
[].
540 -endif.
541
542 -spec extra_tls_opts() -> proplists:proplist().
543 extra_tls_opts() ->
544
:-(
Opts = [{certfile, get_pem_file_path()},
545 {ciphers, get_opt(tls_ciphers)},
546 {protocol_options, get_opt(tls_options)}],
547
:-(
case get_opt(tls_dh_file) of
548 Path when is_binary(Path) ->
549
:-(
[{dhfile, Path} | Opts];
550 none ->
551
:-(
Opts
552 end.
553
554 %% Internal functions: configuration parsing.
555
556 -spec tls_enabled() -> boolean().
557 tls_enabled() ->
558
:-(
lists:any(fun({_IP, _Port, Transport, _ProxyProtocol, _EnableTURN}) ->
559
:-(
(Transport =:= tls) or (Transport =:= auto)
560 end, get_opt(listen)).
561
562 -spec turn_enabled() -> boolean().
563 turn_enabled() ->
564
:-(
lists:any(fun({_IP, _Port, _Transport, _ProxyProtocol, EnableTURN}) ->
565
:-(
EnableTURN =:= true
566 end, get_opt(listen)).
567
568 -spec got_credentials() -> boolean().
569 got_credentials() ->
570
:-(
case get_opt(secret) of
571 Secrets when is_list(Secrets) ->
572
:-(
lists:all(fun(Secret) ->
573
:-(
is_binary(Secret) and (byte_size(Secret) > 0)
574 end, Secrets);
575 Secret when is_binary(Secret), byte_size(Secret) > 0 ->
576
:-(
true;
577 undefined ->
578
:-(
map_size(get_opt(credentials)) > 0
579 end.
580
581 -spec got_relay_addr() -> boolean().
582 got_relay_addr() ->
583
:-(
case get_opt(relay_ipv4_addr) of
584 {_, _, _, _} ->
585
:-(
true;
586 undefined ->
587
:-(
false
588 end.
589
590 -spec check_turn_config() -> ok.
591 check_turn_config() ->
592
:-(
case turn_enabled() of
593 true ->
594
:-(
case {got_relay_addr(),
595 get_opt(relay_min_port),
596 get_opt(relay_max_port)} of
597 {_GotAddr, Min, Max} when Max =< Min ->
598
:-(
exit(turn_config_failure);
599 {false, _Min, _Max} ->
600
:-(
?LOG_WARNING("Specify a 'relay_ipv4_addr' to enable TURN");
601 {true, _Min, _Max} ->
602
:-(
?LOG_DEBUG("TURN configuration seems fine")
603 end;
604 false ->
605
:-(
?LOG_DEBUG("TURN is disabled")
606 end.
607
608 -spec check_proxy_config() -> ok.
609 check_proxy_config() ->
610
:-(
case lists:any(
611 fun({_IP, _Port, Transport, ProxyProtocol, _EnableTURN}) ->
612
:-(
(Transport =:= udp) and (ProxyProtocol =:= true)
613 end, get_opt(listen)) of
614 true ->
615
:-(
exit(proxy_config_failure);
616 false ->
617
:-(
ok
618 end.
619
620 %% Internal functions: configuration reload.
621
622 -spec reload(state()) -> {ok, state()} | {error, term()}.
623 reload(State) ->
624
:-(
case conf:reload_file() of
625 ok ->
626
:-(
try check_pem_file() of
627 ok ->
628
:-(
ok = fast_tls:clear_cache(),
629
:-(
?LOG_INFO("Using new TLS certificate");
630 unchanged ->
631
:-(
?LOG_DEBUG("TLS certificate unchanged")
632 catch exit:Reason1 ->
633
:-(
?LOG_ERROR(format_error(Reason1))
634 end,
635
:-(
try {stop_modules(State), start_modules()} of
636 {ok, Modules} ->
637
:-(
?LOG_DEBUG("Restarted modules"),
638
:-(
{ok, State#eturnal_state{modules = Modules}}
639 catch exit:Reason2 ->
640
:-(
?LOG_ERROR(format_error(Reason2)),
641
:-(
{ok, State}
642 end;
643 {error, Reason} = Err ->
644
:-(
?LOG_ERROR("Cannot reload configuration: ~ts",
645
:-(
[conf:format_error(Reason)]),
646
:-(
Err
647 end.
648
649 -spec apply_config_changes(state(), config_changes()) -> state().
650 apply_config_changes(State, {Changed, New, Removed} = ConfigChanges) ->
651
:-(
if length(Changed) > 0 ->
652
:-(
?LOG_DEBUG("Changed options: ~p", [Changed]);
653 length(Changed) =:= 0 ->
654
:-(
?LOG_DEBUG("No changed options")
655 end,
656
:-(
if length(Removed) > 0 ->
657
:-(
?LOG_DEBUG("Removed options: ~p", [Removed]);
658 length(Removed) =:= 0 ->
659
:-(
?LOG_DEBUG("No removed options")
660 end,
661
:-(
if length(New) > 0 ->
662
:-(
?LOG_DEBUG("New options: ~p", [New]);
663 length(New) =:= 0 ->
664
:-(
?LOG_DEBUG("No new options")
665 end,
666
:-(
try apply_logging_config_changes(ConfigChanges)
667 catch exit:Reason1 ->
668
:-(
?LOG_ERROR(format_error(Reason1))
669 end,
670
:-(
try apply_run_dir_config_changes(ConfigChanges)
671 catch exit:Reason2 ->
672
:-(
?LOG_ERROR(format_error(Reason2))
673 end,
674
:-(
try apply_relay_config_changes(ConfigChanges)
675 catch exit:Reason3 ->
676
:-(
?LOG_ERROR(format_error(Reason3))
677 end,
678
:-(
try apply_listener_config_changes(ConfigChanges, State)
679 catch exit:Reason4 ->
680
:-(
?LOG_ERROR(format_error(Reason4)),
681
:-(
State
682 end.
683
684 -spec apply_logging_config_changes(config_changes()) -> ok.
685 apply_logging_config_changes(ConfigChanges) ->
686
:-(
case logging_config_changed(ConfigChanges) of
687 true ->
688
:-(
?LOG_INFO("Using new logging configuration"),
689
:-(
ok = eturnal_logger:reconfigure();
690 false ->
691
:-(
?LOG_DEBUG("Logging configuration unchanged")
692 end.
693
694 -spec apply_run_dir_config_changes(config_changes()) -> ok.
695 apply_run_dir_config_changes(ConfigChanges) ->
696
:-(
case run_dir_config_changed(ConfigChanges) of
697 true ->
698
:-(
?LOG_INFO("Using new run directory configuration"),
699
:-(
ok = ensure_run_dir(),
700
:-(
case check_pem_file() of
701 ok ->
702
:-(
ok = fast_tls:clear_cache();
703 unchanged ->
704
:-(
ok
705 end;
706 false ->
707
:-(
?LOG_DEBUG("Run directory configuration unchanged")
708 end.
709
710 -spec apply_relay_config_changes(config_changes()) -> ok.
711 apply_relay_config_changes(ConfigChanges) ->
712
:-(
case relay_config_changed(ConfigChanges) of
713 true ->
714
:-(
?LOG_INFO("Using new TURN relay configuration"),
715
:-(
ok = log_relay_addresses();
716 false ->
717
:-(
?LOG_DEBUG("TURN relay configuration unchanged")
718 end.
719
720 -spec apply_listener_config_changes(config_changes(), state()) -> state().
721 apply_listener_config_changes(ConfigChanges, State) ->
722
:-(
case listener_config_changed(ConfigChanges) of
723 true ->
724
:-(
?LOG_INFO("Using new listener configuration"),
725
:-(
ok = check_turn_config(),
726
:-(
ok = check_proxy_config(),
727
:-(
ok = stop_listeners(State),
728
:-(
ok = timer:sleep(500),
729
:-(
Listeners = start_listeners(),
730
:-(
State#eturnal_state{listeners = Listeners};
731 false ->
732
:-(
?LOG_DEBUG("Listener configuration unchanged"),
733
:-(
State
734 end.
735
736 -spec logging_config_changed(config_changes()) -> boolean().
737 logging_config_changed({Changed, New, Removed}) ->
738 2 ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed),
739 2 LoggingKeys = [log_dir,
740 log_level,
741 log_rotate_size,
742 log_rotate_count],
743 2 lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, LoggingKeys).
744
745 -spec run_dir_config_changed(config_changes()) -> boolean().
746 run_dir_config_changed({Changed, New, Removed}) ->
747 4 ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed),
748 4 RunDirKeys = [run_dir],
749 4 lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, RunDirKeys).
750
751 -spec relay_config_changed(config_changes()) -> boolean().
752 relay_config_changed({Changed, New, Removed}) ->
753 4 ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed),
754 4 RelayKeys = [relay_ipv4_addr,
755 relay_ipv6_addr,
756 relay_min_port,
757 relay_max_port],
758 4 lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, RelayKeys).
759
760 -spec listener_config_changed(config_changes()) -> boolean().
761 listener_config_changed({Changed, New, Removed} = ConfigChanges) ->
762 2 case relay_config_changed(ConfigChanges) or
763 run_dir_config_changed(ConfigChanges) of
764 true ->
765
:-(
true;
766 false ->
767 2 ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed),
768 2 ListenerKeys = [listen,
769 max_allocations,
770 max_permissions,
771 max_bps,
772 blacklist,
773 whitelist,
774 blacklist_clients,
775 whitelist_clients,
776 blacklist_peers,
777 whitelist_peers,
778 realm,
779 software_name,
780 tls_options,
781 tls_ciphers,
782 tls_dh_file],
783 2 lists:any(fun(Key) ->
784 19 lists:member(Key, ModifiedKeys)
785 end, ListenerKeys)
786 end.
787
788 %% Internal functions: PEM file handling.
789
790 -spec get_pem_file_path() -> file:filename_all().
791 get_pem_file_path() ->
792
:-(
filename:join(get_opt(run_dir), <<?PEM_FILE_NAME>>).
793
794 -spec check_pem_file() -> ok | unchanged.
795 check_pem_file() ->
796
:-(
case tls_enabled() of
797 true ->
798
:-(
OutFile = get_pem_file_path(),
799
:-(
case {get_opt(tls_crt_file), filelib:last_modified(OutFile)} of
800 {none, OutTime} when OutTime =/= 0 ->
801
:-(
?LOG_DEBUG("Keeping PEM file (~ts)", [OutFile]),
802
:-(
unchanged;
803 {none, OutTime} when OutTime =:= 0 ->
804
:-(
?LOG_WARNING("TLS enabled without 'tls_crt_file', creating "
805
:-(
"self-signed certificate"),
806
:-(
ok = create_self_signed(OutFile);
807 {CrtFile, OutTime} ->
808
:-(
case filelib:last_modified(CrtFile) of
809 CrtTime when CrtTime =< OutTime ->
810
:-(
?LOG_DEBUG("Keeping PEM file (~ts)", [OutFile]),
811
:-(
unchanged;
812 CrtTime when CrtTime =/= 0 -> % Assert to be true.
813
:-(
?LOG_DEBUG("Updating PEM file (~ts)", [OutFile]),
814
:-(
ok = import_pem_file(CrtFile, OutFile)
815 end
816 end;
817 false ->
818
:-(
?LOG_DEBUG("TLS not enabled, ignoring certificate configuration"),
819
:-(
unchanged
820 end.
821
822 -spec import_pem_file(binary(), file:filename_all()) -> ok.
823 import_pem_file(CrtFile, OutFile) ->
824
:-(
try
825
:-(
ok = touch(OutFile),
826
:-(
case get_opt(tls_key_file) of
827 KeyFile when is_binary(KeyFile) ->
828
:-(
ok = copy_file(KeyFile, OutFile, write);
829 none ->
830
:-(
?LOG_INFO("No 'tls_key_file' specified, assuming key in ~ts",
831
:-(
[CrtFile])
832 end,
833
:-(
ok = copy_file(CrtFile, OutFile, append)
834 catch error:{_, {error, Reason}} ->
835
:-(
exit({pem_failure, OutFile, Reason})
836 end.
837
838 -spec copy_file(file:name_all(), file:name_all(), write | append) -> ok.
839 copy_file(Src, Dst, Mode) ->
840
:-(
SrcMode = [read, binary, raw],
841
:-(
DstMode = [Mode, binary, raw],
842
:-(
{ok, _} = file:copy({Src, SrcMode}, {Dst, DstMode}),
843
:-(
?LOG_DEBUG("Copied ~ts into ~ts", [Src, Dst]).
844
845 -spec touch(file:filename_all()) -> ok.
846 touch(File) ->
847
:-(
{ok, Fd} = file:open(File, [append, binary, raw]),
848
:-(
ok = file:close(Fd),
849
:-(
ok = file:change_mode(File, 8#00600).
850
851 %% Internal functions: run directory.
852
853 -spec ensure_run_dir() -> ok.
854 ensure_run_dir() ->
855
:-(
RunDir = get_opt(run_dir),
856
:-(
case filelib:ensure_dir(filename:join(RunDir, <<"file">>)) of
857 ok ->
858
:-(
?LOG_DEBUG("Using run directory ~ts", [RunDir]);
859 {error, Reason} ->
860
:-(
exit({run_dir_failure, create, RunDir, Reason})
861 end.
862
863 -spec clean_run_dir() -> ok.
864 clean_run_dir() ->
865
:-(
PEMFile = get_pem_file_path(),
866
:-(
case filelib:is_regular(PEMFile) of
867 true ->
868
:-(
case file:delete(PEMFile) of
869 ok ->
870
:-(
?LOG_DEBUG("Removed ~ts", [PEMFile]);
871 {error, Reason} ->
872
:-(
exit({run_dir_failure, clean, PEMFile, Reason})
873 end;
874 false ->
875
:-(
?LOG_DEBUG("PEM file doesn't exist: ~ts", [PEMFile])
876 end.
877
878 %% Internal functions: error message formatting.
879
880 -spec format_error(atom() | tuple()) -> binary().
881 format_error({module_failure, Action, Mod, Reason}) ->
882
:-(
format("Failed to ~s ~s: ~p", [Action, Mod, Reason]);
883 format_error({dependency_failure, Mod, Dep}) ->
884
:-(
format("Dependency ~s is missing; install it below ~s, or point ERL_LIBS "
885 "to it, or disable ~s", [Dep, code:lib_dir(), Mod]);
886 format_error({listener_failure, Action, IP, Port, Transport, Reason}) ->
887
:-(
format("Cannot ~s listening on ~s (~s): ~s",
888 [Action, eturnal_misc:addr_to_str(IP, Port), Transport,
889 inet:format_error(Reason)]);
890 format_error({run_dir_failure, Action, RunDir, Reason}) ->
891
:-(
format("Cannot ~s run directory ~ts: ~ts",
892 [Action, RunDir, file:format_error(Reason)]);
893 format_error({pem_failure, File, Reason}) ->
894
:-(
format("Cannot create PEM file ~ts: ~ts",
895 [File, file:format_error(Reason)]);
896 format_error({otp_too_old, Key, Value, Vsn}) ->
897
:-(
format("Setting '~s: ~s' requires Erlang/OTP ~B or later",
898 [Key, Value, Vsn]);
899 format_error(proxy_config_failure) ->
900
:-(
<<"The 'proxy_protocol' ist not supported for 'udp'">>;
901 format_error(turn_config_failure) ->
902
:-(
<<"The 'relay_max_port' must be larger than the 'relay_min_port'">>;
903 format_error(_Unknown) ->
904
:-(
<<"Unknown error">>.
905
906 -spec format(io:format(), [term()]) -> binary().
907 format(Fmt, Data) ->
908
:-(
case unicode:characters_to_binary(io_lib:format(Fmt, Data)) of
909 Bin when is_binary(Bin) ->
910
:-(
Bin;
911 {_, _, _} = Err ->
912
:-(
erlang:error(Err)
913 end.
914
915 %% EUnit tests.
916
917 -ifdef(EUNIT).
918 config_change_test_() ->
919 4 [?_assert(logging_config_changed({[{log_level, info}], [], []})),
920 1 ?_assert(run_dir_config_changed({[{run_dir, <<"run">>}], [], []})),
921 1 ?_assert(relay_config_changed({[{relay_min_port, 50000}], [], []})),
922 1 ?_assert(listener_config_changed({[{max_bps, 42}], [], []})),
923 1 ?_assertNot(logging_config_changed({[{strict_expiry, false}], [], []})),
924 1 ?_assertNot(run_dir_config_changed({[{strict_expiry, false}], [], []})),
925 1 ?_assertNot(relay_config_changed({[{strict_expiry, false}], [], []})),
926 1 ?_assertNot(listener_config_changed({[{strict_expiry, false}], [], []}))].
927 -endif.
Line Hits Source