/home/runner/work/eturnal/eturnal/_build/test/cover/aggregate/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 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_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_info, _From, State) ->
125 1 Info = eturnal_misc:info(),
126 1 {reply, {ok, Info}, State};
127 handle_call(get_version, _From, State) ->
128 1 Version = eturnal_misc:version(),
129 1 {reply, {ok, Version}, State};
130 handle_call(get_loglevel, _From, State) ->
131 2 Level = eturnal_logger:get_level(),
132 2 {reply, {ok, Level}, State};
133 handle_call({set_loglevel, Level}, _From, State) ->
134 1 try
135 1 ok = eturnal_logger:set_level(Level),
136 1 {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 13 case {get_opt(secret), is_dynamic_username(Username)} of
142 {[Secret | _Secrets], true} ->
143 11 Password = derive_password(Username, [Secret]),
144 11 {reply, {ok, Password}, State};
145 {_, _} ->
146 2 case maps:get(Username, get_opt(credentials), undefined) of
147 Password when is_binary(Password) ->
148 1 {reply, {ok, Password}, State};
149 undefined ->
150 1 {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 1 ?LOG_DEBUG("Terminating ~s (~p)", [?MODULE, Reason]),
185 1 try stop_listeners(State)
186 catch exit:Reason1 ->
187
:-(
?LOG_ERROR(format_error(Reason1))
188 end,
189 1 try stop_modules(State)
190 catch exit:Reason2 ->
191
:-(
?LOG_ERROR(format_error(Reason2))
192 end,
193 1 try clean_run_dir()
194 catch exit:Reason3 ->
195
:-(
?LOG_ERROR(format_error(Reason3))
196 end,
197 1 _ = eturnal_module:terminate(),
198 1 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 1 case config_is_loaded() of
212 true ->
213 1 ?LOG_DEBUG("Configuration has been loaded successfully"),
214 1 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 1 try eturnal:get_opt(realm) of
223 Realm when is_binary(Realm) ->
224 1 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 9 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 4 [Expiration | _Suffix] = binary:split(Username, <<$:>>),
239 4 try binary_to_integer(Expiration) of
240 ExpireTime ->
241 2 case erlang:system_time(second) of
242 Now when Now < ExpireTime ->
243 2 ?LOG_DEBUG("Deriving password for: ~ts", [Username]),
244 2 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 2 ?LOG_DEBUG("Looking up password for: ~ts", [Username]),
258 2 case maps:get(Username, get_opt(credentials), undefined) of
259 Password when is_binary(Password) ->
260 2 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 120 {ok, Val} = application:get_env(eturnal, Opt),
272 120 Val.
273
274 %% API: create self-signed certificate.
275
276 -spec create_self_signed(file:filename_all()) -> ok.
277 create_self_signed(File) ->
278 2 try
279 2 PEM = eturnal_cert:create(get_opt(realm)),
280 2 ok = touch(File),
281 2 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 13 case string:to_integer(Username) of
318 {N, <<":", _Rest/binary>>} when is_integer(N), N > 0 ->
319 10 true;
320 {N, <<>>} when is_integer(N), N > 0 ->
321 1 true;
322 {_, _} ->
323 2 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 13 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 1 Min = get_opt(relay_min_port),
344 1 Max = get_opt(relay_max_port),
345 1 case get_opt(relay_ipv4_addr) of
346 {_, _, _, _} = Addr4 ->
347 1 ?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 1 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 1 ?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 1 [Name, Host] = string:split(atom_to_list(node()), "@"),
364 % The 'catch' calms Dialyzer on OTP 21 (even though we don't match 'EXIT').
365 1 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 1 ?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 3 lists:map(
380 fun({Mod, _Opts}) ->
381 9 case eturnal_module:start(Mod) of
382 ok ->
383 9 ?LOG_INFO("Started ~s", [Mod]),
384 9 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 3 lists:foreach(
393 fun(Mod) ->
394 9 case eturnal_module:stop(Mod) of
395 ok ->
396 9 ?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 1 Opts = lists:filtermap(
407 fun({InKey, OutKey}) ->
408 9 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 1 lists:map(
414 fun({IP, Port, Transport, ProxyProtocol, EnableTURN}) ->
415 4 Opts1 = tls_opts(Transport) ++ Opts,
416 4 Opts2 = turn_opts(EnableTURN) ++ Opts1,
417 4 Opts3 = proxy_opts(ProxyProtocol) ++ Opts2,
418 4 ?LOG_DEBUG("Starting listener ~s (~s) with options:~n~p",
419 [eturnal_misc:addr_to_str(IP, Port), Transport,
420
:-(
Opts3]),
421 4 InfoArgs = [eturnal_misc:addr_to_str(IP, Port), Transport,
422 describe_listener(EnableTURN)],
423 4 case stun_listener:add_listener(IP, Port, Transport, Opts3) of
424 ok ->
425 4 ?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 4 {IP, Port, Transport}
433 end, get_opt(listen)).
434
435 -spec stop_listeners(state()) -> ok.
436 stop_listeners(#eturnal_state{listeners = Listeners}) ->
437 1 lists:foreach(
438 fun({IP, Port, Transport}) ->
439 4 case stun_listener:del_listener(IP, Port, Transport) of
440 ok ->
441 4 ?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 1 <<"STUN/TURN">>;
453 describe_listener(false = _EnableTURN) ->
454 3 <<"STUN only">>.
455
456 -spec opt_map() -> [{atom(), atom()}].
457 opt_map() ->
458 1 [{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 1 false; % The 'stun' application currently wouldn't accept 'undefined'.
471 opt_filter(Opt) ->
472 8 {true, Opt}.
473
474 -spec turn_opts(boolean()) -> proplists:proplist().
475 turn_opts(EnableTURN) ->
476 4 case {EnableTURN, got_credentials(), got_relay_addr()} of
477 {true, true, true} ->
478 1 [{use_turn, true},
479 {auth_type, user}];
480 {_, _, _} ->
481 3 [{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 4 [].
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 1 case {eturnal:get_opt(blacklist),
496 eturnal:get_opt(blacklist_clients),
497 eturnal:get_opt(blacklist_peers)} of
498 {[], Clients, Peers} ->
499 1 [{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 1 case {eturnal:get_opt(whitelist),
513 eturnal:get_opt(whitelist_clients),
514 eturnal:get_opt(whitelist_peers)} of
515 {[], Clients, Peers} ->
516 1 [{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 1 [{tls, true} | extra_tls_opts()];
536 tls_opts(auto) ->
537 1 [{tls, optional} | extra_tls_opts()];
538 tls_opts(_) ->
539 2 [].
540 -endif.
541
542 -spec extra_tls_opts() -> proplists:proplist().
543 extra_tls_opts() ->
544 2 Opts = [{certfile, get_pem_file_path()},
545 {ciphers, get_opt(tls_ciphers)},
546 {protocol_options, get_opt(tls_options)}],
547 2 case get_opt(tls_dh_file) of
548 Path when is_binary(Path) ->
549
:-(
[{dhfile, Path} | Opts];
550 none ->
551 2 Opts
552 end.
553
554 %% Internal functions: configuration parsing.
555
556 -spec tls_enabled() -> boolean().
557 tls_enabled() ->
558 3 lists:any(fun({_IP, _Port, Transport, _ProxyProtocol, _EnableTURN}) ->
559 9 (Transport =:= tls) or (Transport =:= auto)
560 end, get_opt(listen)).
561
562 -spec turn_enabled() -> boolean().
563 turn_enabled() ->
564 1 lists:any(fun({_IP, _Port, _Transport, _ProxyProtocol, EnableTURN}) ->
565 1 EnableTURN =:= true
566 end, get_opt(listen)).
567
568 -spec got_credentials() -> boolean().
569 got_credentials() ->
570 4 case get_opt(secret) of
571 Secrets when is_list(Secrets) ->
572 4 lists:all(fun(Secret) ->
573 4 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 5 case get_opt(relay_ipv4_addr) of
584 {_, _, _, _} ->
585 5 true;
586 undefined ->
587
:-(
false
588 end.
589
590 -spec check_turn_config() -> ok.
591 check_turn_config() ->
592 1 case turn_enabled() of
593 true ->
594 1 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 1 ?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 1 case lists:any(
611 fun({_IP, _Port, Transport, ProxyProtocol, _EnableTURN}) ->
612 4 (Transport =:= udp) and (ProxyProtocol =:= true)
613 end, get_opt(listen)) of
614 true ->
615
:-(
exit(proxy_config_failure);
616 false ->
617 1 ok
618 end.
619
620 %% Internal functions: configuration reload.
621
622 -spec reload(state()) -> {ok, state()} | {error, term()}.
623 reload(State) ->
624 2 case conf:reload_file() of
625 ok ->
626 2 try check_pem_file() of
627 ok ->
628 1 ok = fast_tls:clear_cache(),
629 1 ?LOG_INFO("Using new TLS certificate");
630 unchanged ->
631 1 ?LOG_DEBUG("TLS certificate unchanged")
632 catch exit:Reason1 ->
633
:-(
?LOG_ERROR(format_error(Reason1))
634 end,
635 2 try {stop_modules(State), start_modules()} of
636 {ok, Modules} ->
637 2 ?LOG_DEBUG("Restarted modules"),
638 2 {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 6 filename:join(get_opt(run_dir), <<?PEM_FILE_NAME>>).
793
794 -spec check_pem_file() -> ok | unchanged.
795 check_pem_file() ->
796 3 case tls_enabled() of
797 true ->
798 3 OutFile = get_pem_file_path(),
799 3 case {get_opt(tls_crt_file), filelib:last_modified(OutFile)} of
800 {none, OutTime} when OutTime =/= 0 ->
801 1 ?LOG_DEBUG("Keeping PEM file (~ts)", [OutFile]),
802 1 unchanged;
803 {none, OutTime} when OutTime =:= 0 ->
804 2 ?LOG_WARNING("TLS enabled without 'tls_crt_file', creating "
805
:-(
"self-signed certificate"),
806 2 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 2 {ok, Fd} = file:open(File, [append, binary, raw]),
848 2 ok = file:close(Fd),
849 2 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 1 RunDir = get_opt(run_dir),
856 1 case filelib:ensure_dir(filename:join(RunDir, <<"file">>)) of
857 ok ->
858 1 ?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 1 PEMFile = get_pem_file_path(),
866 1 case filelib:is_regular(PEMFile) of
867 true ->
868 1 case file:delete(PEMFile) of
869 ok ->
870 1 ?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