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

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