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_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 |
:-( |
{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 |
|
% The 'catch' calms Dialyzer on OTP 21 (even though we don't match 'EXIT'). |
367 |
1 |
case catch erl_epmd:port_please(Name, Host, timer:seconds(10)) of |
368 |
|
{port, Port, Version} -> |
369 |
:-( |
?LOG_INFO("Listening on ~s:~B (tcp) (Erlang protocol version ~B)", |
370 |
:-( |
[Host, Port, Version]); |
371 |
|
{error, Reason} -> |
372 |
:-( |
?LOG_INFO("Cannot determine control query port: ~p", [Reason]); |
373 |
|
Reason when is_atom(Reason) -> |
374 |
1 |
?LOG_INFO("Cannot determine control query port: ~s", [Reason]) |
375 |
|
end. |
376 |
|
|
377 |
|
%% Internal functions: module startup/shutdown. |
378 |
|
|
379 |
|
-spec start_modules() -> modules(). |
380 |
|
start_modules() -> |
381 |
3 |
lists:map( |
382 |
|
fun({Mod, _Opts}) -> |
383 |
9 |
case eturnal_module:start(Mod) of |
384 |
|
ok -> |
385 |
9 |
?LOG_INFO("Started ~s", [Mod]), |
386 |
9 |
Mod; |
387 |
|
{error, Reason} -> |
388 |
:-( |
exit({module_failure, start, Mod, Reason}) |
389 |
|
end |
390 |
|
end, maps:to_list(get_opt(modules))). |
391 |
|
|
392 |
|
-spec stop_modules(state()) -> ok. |
393 |
|
stop_modules(#eturnal_state{modules = Modules}) -> |
394 |
3 |
lists:foreach( |
395 |
|
fun(Mod) -> |
396 |
9 |
case eturnal_module:stop(Mod) of |
397 |
|
ok -> |
398 |
9 |
?LOG_INFO("Stopped ~s", [Mod]); |
399 |
|
{error, Reason} -> |
400 |
:-( |
exit({module_failure, stop, Mod, Reason}) |
401 |
|
end |
402 |
|
end, Modules). |
403 |
|
|
404 |
|
%% Internal functions: listener startup/shutdown. |
405 |
|
|
406 |
|
-spec start_listeners() -> listeners(). |
407 |
|
start_listeners() -> |
408 |
1 |
Opts = lists:filtermap( |
409 |
|
fun({InKey, OutKey}) -> |
410 |
9 |
opt_filter({OutKey, get_opt(InKey)}) |
411 |
|
end, opt_map()) ++ [{auth_fun, fun ?MODULE:get_password/2}, |
412 |
|
{hook_fun, fun ?MODULE:run_hook/2}] |
413 |
|
++ blacklist_opts() |
414 |
|
++ whitelist_opts(), |
415 |
1 |
lists:map( |
416 |
|
fun({IP, Port, Transport, ProxyProtocol, EnableTURN}) -> |
417 |
4 |
Opts1 = tls_opts(Transport) ++ Opts, |
418 |
4 |
Opts2 = turn_opts(EnableTURN) ++ Opts1, |
419 |
4 |
Opts3 = proxy_opts(ProxyProtocol) ++ Opts2, |
420 |
4 |
?LOG_DEBUG("Starting listener ~s (~s) with options:~n~p", |
421 |
|
[eturnal_misc:addr_to_str(IP, Port), Transport, |
422 |
:-( |
Opts3]), |
423 |
4 |
InfoArgs = [eturnal_misc:addr_to_str(IP, Port), Transport, |
424 |
|
describe_listener(EnableTURN)], |
425 |
4 |
case stun_listener:add_listener(IP, Port, Transport, Opts3) of |
426 |
|
ok -> |
427 |
4 |
?LOG_INFO("Listening on ~s (~s) (~s)", InfoArgs); |
428 |
|
{error, already_started} -> |
429 |
:-( |
?LOG_INFO("Already listening on ~s (~s) (~s)", InfoArgs); |
430 |
|
{error, Reason} -> |
431 |
:-( |
exit({listener_failure, start, IP, Port, Transport, |
432 |
|
Reason}) |
433 |
|
end, |
434 |
4 |
{IP, Port, Transport} |
435 |
|
end, get_opt(listen)). |
436 |
|
|
437 |
|
-spec stop_listeners(state()) -> ok. |
438 |
|
stop_listeners(#eturnal_state{listeners = Listeners}) -> |
439 |
1 |
lists:foreach( |
440 |
|
fun({IP, Port, Transport}) -> |
441 |
4 |
case stun_listener:del_listener(IP, Port, Transport) of |
442 |
|
ok -> |
443 |
4 |
?LOG_INFO("Stopped listening on ~s (~s)", |
444 |
|
[eturnal_misc:addr_to_str(IP, Port), |
445 |
:-( |
Transport]); |
446 |
|
{error, Reason} -> |
447 |
:-( |
exit({listener_failure, stop, IP, Port, Transport, |
448 |
|
Reason}) |
449 |
|
end |
450 |
|
end, Listeners). |
451 |
|
|
452 |
|
-spec describe_listener(boolean()) -> binary(). |
453 |
|
describe_listener(true = _EnableTURN) -> |
454 |
1 |
<<"STUN/TURN">>; |
455 |
|
describe_listener(false = _EnableTURN) -> |
456 |
3 |
<<"STUN only">>. |
457 |
|
|
458 |
|
-spec opt_map() -> [{atom(), atom()}]. |
459 |
|
opt_map() -> |
460 |
1 |
[{relay_ipv4_addr, turn_ipv4_address}, |
461 |
|
{relay_ipv6_addr, turn_ipv6_address}, |
462 |
|
{relay_min_port, turn_min_port}, |
463 |
|
{relay_max_port, turn_max_port}, |
464 |
|
{max_allocations, turn_max_allocations}, |
465 |
|
{max_permissions, turn_max_permissions}, |
466 |
|
{max_bps, shaper}, |
467 |
|
{realm, auth_realm}, |
468 |
|
{software_name, server_name}]. |
469 |
|
|
470 |
|
-spec opt_filter(Opt) -> {true, Opt} | false when Opt :: {option(), value()}. |
471 |
|
opt_filter({turn_ipv6_address, undefined}) -> |
472 |
1 |
false; % The 'stun' application currently wouldn't accept 'undefined'. |
473 |
|
opt_filter(Opt) -> |
474 |
8 |
{true, Opt}. |
475 |
|
|
476 |
|
-spec turn_opts(boolean()) -> proplists:proplist(). |
477 |
|
turn_opts(EnableTURN) -> |
478 |
4 |
case {EnableTURN, got_credentials(), got_relay_addr()} of |
479 |
|
{true, true, true} -> |
480 |
1 |
[{use_turn, true}, |
481 |
|
{auth_type, user}]; |
482 |
|
{_, _, _} -> |
483 |
3 |
[{use_turn, false}, |
484 |
|
{auth_type, anonymous}] |
485 |
|
end. |
486 |
|
|
487 |
|
-spec proxy_opts(boolean()) -> proplists:proplist(). |
488 |
|
proxy_opts(true = _ProxyProtocol) -> |
489 |
:-( |
[proxy_protocol]; |
490 |
|
proxy_opts(false = _ProxyProtocol) -> |
491 |
4 |
[]. |
492 |
|
|
493 |
|
%% This function can be removed in favor of opt_map/0 entries once the |
494 |
|
%% 'blacklist' option is removed. |
495 |
|
-spec blacklist_opts() -> proplists:proplist(). |
496 |
|
blacklist_opts() -> |
497 |
1 |
case {eturnal:get_opt(blacklist), |
498 |
|
eturnal:get_opt(blacklist_clients), |
499 |
|
eturnal:get_opt(blacklist_peers)} of |
500 |
|
{[], Clients, Peers} -> |
501 |
1 |
[{turn_blacklist_clients, Clients}, |
502 |
|
{turn_blacklist_peers, Peers}]; |
503 |
|
{Blacklist, Clients, Peers} -> |
504 |
:-( |
?LOG_WARNING("The 'blacklist' option is deprecated"), |
505 |
:-( |
?LOG_WARNING("Use 'blacklist_clients' and/or 'blacklist_peers'"), |
506 |
:-( |
[{turn_blacklist_clients, lists:usort(Clients ++ Blacklist)}, |
507 |
|
{turn_blacklist_peers, lists:usort(Peers ++ Blacklist)}] |
508 |
|
end. |
509 |
|
|
510 |
|
%% This function can be removed in favor of opt_map/0 entries once the |
511 |
|
%% 'whitelist' option is removed. |
512 |
|
-spec whitelist_opts() -> proplists:proplist(). |
513 |
|
whitelist_opts() -> |
514 |
1 |
case {eturnal:get_opt(whitelist), |
515 |
|
eturnal:get_opt(whitelist_clients), |
516 |
|
eturnal:get_opt(whitelist_peers)} of |
517 |
|
{[], Clients, Peers} -> |
518 |
1 |
[{turn_whitelist_clients, Clients}, |
519 |
|
{turn_whitelist_peers, Peers}]; |
520 |
|
{Whitelist, Clients, Peers} -> |
521 |
:-( |
?LOG_WARNING("The 'whitelist' option is deprecated"), |
522 |
:-( |
?LOG_WARNING("Use 'whitelist_clients' and/or 'whitelist_peers'"), |
523 |
:-( |
[{turn_whitelist_clients, lists:usort(Clients ++ Whitelist)}, |
524 |
|
{turn_whitelist_peers, lists:usort(Peers ++ Whitelist)}] |
525 |
|
end. |
526 |
|
|
527 |
|
-spec tls_opts(transport()) -> proplists:proplist(). |
528 |
|
-ifdef(old_inet_backend). |
529 |
|
tls_opts(tls) -> |
530 |
|
[{tls, true} | extra_tls_opts()]; |
531 |
|
tls_opts(auto) -> |
532 |
|
exit({otp_too_old, transport, auto, 23}); |
533 |
|
tls_opts(_) -> |
534 |
|
[]. |
535 |
|
-else. |
536 |
|
tls_opts(tls) -> |
537 |
1 |
[{tls, true} | extra_tls_opts()]; |
538 |
|
tls_opts(auto) -> |
539 |
1 |
[{tls, optional} | extra_tls_opts()]; |
540 |
|
tls_opts(_) -> |
541 |
2 |
[]. |
542 |
|
-endif. |
543 |
|
|
544 |
|
-spec extra_tls_opts() -> proplists:proplist(). |
545 |
|
extra_tls_opts() -> |
546 |
2 |
Opts = [{certfile, get_pem_file_path()}, |
547 |
|
{ciphers, get_opt(tls_ciphers)}, |
548 |
|
{protocol_options, get_opt(tls_options)}], |
549 |
2 |
case get_opt(tls_dh_file) of |
550 |
|
Path when is_binary(Path) -> |
551 |
:-( |
[{dhfile, Path} | Opts]; |
552 |
|
none -> |
553 |
2 |
Opts |
554 |
|
end. |
555 |
|
|
556 |
|
%% Internal functions: configuration parsing. |
557 |
|
|
558 |
|
-spec tls_enabled() -> boolean(). |
559 |
|
tls_enabled() -> |
560 |
3 |
lists:any(fun({_IP, _Port, Transport, _ProxyProtocol, _EnableTURN}) -> |
561 |
9 |
(Transport =:= tls) or (Transport =:= auto) |
562 |
|
end, get_opt(listen)). |
563 |
|
|
564 |
|
-spec turn_enabled() -> boolean(). |
565 |
|
turn_enabled() -> |
566 |
1 |
lists:any(fun({_IP, _Port, _Transport, _ProxyProtocol, EnableTURN}) -> |
567 |
1 |
EnableTURN =:= true |
568 |
|
end, get_opt(listen)). |
569 |
|
|
570 |
|
-spec got_credentials() -> boolean(). |
571 |
|
got_credentials() -> |
572 |
4 |
case get_opt(secret) of |
573 |
|
Secrets when is_list(Secrets) -> |
574 |
4 |
lists:all(fun(Secret) -> |
575 |
4 |
is_binary(Secret) and (byte_size(Secret) > 0) |
576 |
|
end, Secrets); |
577 |
|
Secret when is_binary(Secret), byte_size(Secret) > 0 -> |
578 |
:-( |
true; |
579 |
|
undefined -> |
580 |
:-( |
map_size(get_opt(credentials)) > 0 |
581 |
|
end. |
582 |
|
|
583 |
|
-spec got_relay_addr() -> boolean(). |
584 |
|
got_relay_addr() -> |
585 |
5 |
case get_opt(relay_ipv4_addr) of |
586 |
|
{_, _, _, _} -> |
587 |
5 |
true; |
588 |
|
undefined -> |
589 |
:-( |
false |
590 |
|
end. |
591 |
|
|
592 |
|
-spec check_turn_config() -> ok. |
593 |
|
check_turn_config() -> |
594 |
1 |
case turn_enabled() of |
595 |
|
true -> |
596 |
1 |
case {got_relay_addr(), |
597 |
|
get_opt(relay_min_port), |
598 |
|
get_opt(relay_max_port)} of |
599 |
|
{_GotAddr, Min, Max} when Max =< Min -> |
600 |
:-( |
exit(turn_config_failure); |
601 |
|
{false, _Min, _Max} -> |
602 |
:-( |
?LOG_WARNING("Specify a 'relay_ipv4_addr' to enable TURN"); |
603 |
|
{true, _Min, _Max} -> |
604 |
1 |
?LOG_DEBUG("TURN configuration seems fine") |
605 |
|
end; |
606 |
|
false -> |
607 |
:-( |
?LOG_DEBUG("TURN is disabled") |
608 |
|
end. |
609 |
|
|
610 |
|
-spec check_proxy_config() -> ok. |
611 |
|
check_proxy_config() -> |
612 |
1 |
case lists:any( |
613 |
|
fun({_IP, _Port, Transport, ProxyProtocol, _EnableTURN}) -> |
614 |
4 |
(Transport =:= udp) and (ProxyProtocol =:= true) |
615 |
|
end, get_opt(listen)) of |
616 |
|
true -> |
617 |
:-( |
exit(proxy_config_failure); |
618 |
|
false -> |
619 |
1 |
ok |
620 |
|
end. |
621 |
|
|
622 |
|
%% Internal functions: configuration reload. |
623 |
|
|
624 |
|
-spec reload(state()) -> {ok, state()} | {error, term()}. |
625 |
|
reload(State) -> |
626 |
2 |
case conf:reload_file() of |
627 |
|
ok -> |
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 |
:-( |
if length(Changed) > 0 -> |
654 |
:-( |
?LOG_DEBUG("Changed options: ~p", [Changed]); |
655 |
|
length(Changed) =:= 0 -> |
656 |
:-( |
?LOG_DEBUG("No changed options") |
657 |
|
end, |
658 |
:-( |
if length(Removed) > 0 -> |
659 |
:-( |
?LOG_DEBUG("Removed options: ~p", [Removed]); |
660 |
|
length(Removed) =:= 0 -> |
661 |
:-( |
?LOG_DEBUG("No removed options") |
662 |
|
end, |
663 |
:-( |
if length(New) > 0 -> |
664 |
:-( |
?LOG_DEBUG("New options: ~p", [New]); |
665 |
|
length(New) =:= 0 -> |
666 |
:-( |
?LOG_DEBUG("No new options") |
667 |
|
end, |
668 |
:-( |
try apply_logging_config_changes(ConfigChanges) |
669 |
|
catch exit:Reason1 -> |
670 |
:-( |
?LOG_ERROR(format_error(Reason1)) |
671 |
|
end, |
672 |
:-( |
try apply_run_dir_config_changes(ConfigChanges) |
673 |
|
catch exit:Reason2 -> |
674 |
:-( |
?LOG_ERROR(format_error(Reason2)) |
675 |
|
end, |
676 |
:-( |
try apply_relay_config_changes(ConfigChanges) |
677 |
|
catch exit:Reason3 -> |
678 |
:-( |
?LOG_ERROR(format_error(Reason3)) |
679 |
|
end, |
680 |
:-( |
try apply_listener_config_changes(ConfigChanges, State) |
681 |
|
catch exit:Reason4 -> |
682 |
:-( |
?LOG_ERROR(format_error(Reason4)), |
683 |
:-( |
State |
684 |
|
end. |
685 |
|
|
686 |
|
-spec apply_logging_config_changes(config_changes()) -> ok. |
687 |
|
apply_logging_config_changes(ConfigChanges) -> |
688 |
:-( |
case logging_config_changed(ConfigChanges) of |
689 |
|
true -> |
690 |
:-( |
?LOG_INFO("Using new logging configuration"), |
691 |
:-( |
ok = eturnal_logger:reconfigure(); |
692 |
|
false -> |
693 |
:-( |
?LOG_DEBUG("Logging configuration unchanged") |
694 |
|
end. |
695 |
|
|
696 |
|
-spec apply_run_dir_config_changes(config_changes()) -> ok. |
697 |
|
apply_run_dir_config_changes(ConfigChanges) -> |
698 |
:-( |
case run_dir_config_changed(ConfigChanges) of |
699 |
|
true -> |
700 |
:-( |
?LOG_INFO("Using new run directory configuration"), |
701 |
:-( |
ok = ensure_run_dir(), |
702 |
:-( |
case check_pem_file() of |
703 |
|
ok -> |
704 |
:-( |
ok = fast_tls:clear_cache(); |
705 |
|
unchanged -> |
706 |
:-( |
ok |
707 |
|
end; |
708 |
|
false -> |
709 |
:-( |
?LOG_DEBUG("Run directory configuration unchanged") |
710 |
|
end. |
711 |
|
|
712 |
|
-spec apply_relay_config_changes(config_changes()) -> ok. |
713 |
|
apply_relay_config_changes(ConfigChanges) -> |
714 |
:-( |
case relay_config_changed(ConfigChanges) of |
715 |
|
true -> |
716 |
:-( |
?LOG_INFO("Using new TURN relay configuration"), |
717 |
:-( |
ok = log_relay_addresses(); |
718 |
|
false -> |
719 |
:-( |
?LOG_DEBUG("TURN relay configuration unchanged") |
720 |
|
end. |
721 |
|
|
722 |
|
-spec apply_listener_config_changes(config_changes(), state()) -> state(). |
723 |
|
apply_listener_config_changes(ConfigChanges, State) -> |
724 |
:-( |
case listener_config_changed(ConfigChanges) of |
725 |
|
true -> |
726 |
:-( |
?LOG_INFO("Using new listener configuration"), |
727 |
:-( |
ok = check_turn_config(), |
728 |
:-( |
ok = check_proxy_config(), |
729 |
:-( |
ok = stop_listeners(State), |
730 |
:-( |
ok = timer:sleep(500), |
731 |
:-( |
Listeners = start_listeners(), |
732 |
:-( |
State#eturnal_state{listeners = Listeners}; |
733 |
|
false -> |
734 |
:-( |
?LOG_DEBUG("Listener configuration unchanged"), |
735 |
:-( |
State |
736 |
|
end. |
737 |
|
|
738 |
|
-spec logging_config_changed(config_changes()) -> boolean(). |
739 |
|
logging_config_changed({Changed, New, Removed}) -> |
740 |
2 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
741 |
2 |
LoggingKeys = [log_dir, |
742 |
|
log_level, |
743 |
|
log_rotate_size, |
744 |
|
log_rotate_count], |
745 |
2 |
lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, LoggingKeys). |
746 |
|
|
747 |
|
-spec run_dir_config_changed(config_changes()) -> boolean(). |
748 |
|
run_dir_config_changed({Changed, New, Removed}) -> |
749 |
4 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
750 |
4 |
RunDirKeys = [run_dir], |
751 |
4 |
lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, RunDirKeys). |
752 |
|
|
753 |
|
-spec relay_config_changed(config_changes()) -> boolean(). |
754 |
|
relay_config_changed({Changed, New, Removed}) -> |
755 |
4 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
756 |
4 |
RelayKeys = [relay_ipv4_addr, |
757 |
|
relay_ipv6_addr, |
758 |
|
relay_min_port, |
759 |
|
relay_max_port], |
760 |
4 |
lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, RelayKeys). |
761 |
|
|
762 |
|
-spec listener_config_changed(config_changes()) -> boolean(). |
763 |
|
listener_config_changed({Changed, New, Removed} = ConfigChanges) -> |
764 |
2 |
case relay_config_changed(ConfigChanges) or |
765 |
|
run_dir_config_changed(ConfigChanges) of |
766 |
|
true -> |
767 |
:-( |
true; |
768 |
|
false -> |
769 |
2 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
770 |
2 |
ListenerKeys = [listen, |
771 |
|
max_allocations, |
772 |
|
max_permissions, |
773 |
|
max_bps, |
774 |
|
blacklist, |
775 |
|
whitelist, |
776 |
|
blacklist_clients, |
777 |
|
whitelist_clients, |
778 |
|
blacklist_peers, |
779 |
|
whitelist_peers, |
780 |
|
realm, |
781 |
|
software_name, |
782 |
|
tls_options, |
783 |
|
tls_ciphers, |
784 |
|
tls_dh_file], |
785 |
2 |
lists:any(fun(Key) -> |
786 |
19 |
lists:member(Key, ModifiedKeys) |
787 |
|
end, ListenerKeys) |
788 |
|
end. |
789 |
|
|
790 |
|
%% Internal functions: PEM file handling. |
791 |
|
|
792 |
|
-spec get_pem_file_path() -> file:filename_all(). |
793 |
|
get_pem_file_path() -> |
794 |
6 |
filename:join(get_opt(run_dir), <<?PEM_FILE_NAME>>). |
795 |
|
|
796 |
|
-spec check_pem_file() -> ok | unchanged. |
797 |
|
check_pem_file() -> |
798 |
3 |
case tls_enabled() of |
799 |
|
true -> |
800 |
3 |
OutFile = get_pem_file_path(), |
801 |
3 |
case {get_opt(tls_crt_file), filelib:last_modified(OutFile)} of |
802 |
|
{none, OutTime} when OutTime =/= 0 -> |
803 |
1 |
?LOG_DEBUG("Keeping PEM file (~ts)", [OutFile]), |
804 |
1 |
unchanged; |
805 |
|
{none, OutTime} when OutTime =:= 0 -> |
806 |
2 |
?LOG_WARNING("TLS enabled without 'tls_crt_file', creating " |
807 |
:-( |
"self-signed certificate"), |
808 |
2 |
ok = create_self_signed(OutFile); |
809 |
|
{CrtFile, OutTime} -> |
810 |
:-( |
case filelib:last_modified(CrtFile) of |
811 |
|
CrtTime when CrtTime =< OutTime -> |
812 |
:-( |
?LOG_DEBUG("Keeping PEM file (~ts)", [OutFile]), |
813 |
:-( |
unchanged; |
814 |
|
CrtTime when CrtTime =/= 0 -> % Assert to be true. |
815 |
:-( |
?LOG_DEBUG("Updating PEM file (~ts)", [OutFile]), |
816 |
:-( |
ok = import_pem_file(CrtFile, OutFile) |
817 |
|
end |
818 |
|
end; |
819 |
|
false -> |
820 |
:-( |
?LOG_DEBUG("TLS not enabled, ignoring certificate configuration"), |
821 |
:-( |
unchanged |
822 |
|
end. |
823 |
|
|
824 |
|
-spec import_pem_file(binary(), file:filename_all()) -> ok. |
825 |
|
import_pem_file(CrtFile, OutFile) -> |
826 |
:-( |
try |
827 |
:-( |
ok = touch(OutFile), |
828 |
:-( |
case get_opt(tls_key_file) of |
829 |
|
KeyFile when is_binary(KeyFile) -> |
830 |
:-( |
ok = copy_file(KeyFile, OutFile, write); |
831 |
|
none -> |
832 |
:-( |
?LOG_INFO("No 'tls_key_file' specified, assuming key in ~ts", |
833 |
:-( |
[CrtFile]) |
834 |
|
end, |
835 |
:-( |
ok = copy_file(CrtFile, OutFile, append) |
836 |
|
catch error:{_, {error, Reason}} -> |
837 |
:-( |
exit({pem_failure, OutFile, Reason}) |
838 |
|
end. |
839 |
|
|
840 |
|
-spec copy_file(file:name_all(), file:name_all(), write | append) -> ok. |
841 |
|
copy_file(Src, Dst, Mode) -> |
842 |
:-( |
SrcMode = [read, binary, raw], |
843 |
:-( |
DstMode = [Mode, binary, raw], |
844 |
:-( |
{ok, _} = file:copy({Src, SrcMode}, {Dst, DstMode}), |
845 |
:-( |
?LOG_DEBUG("Copied ~ts into ~ts", [Src, Dst]). |
846 |
|
|
847 |
|
-spec touch(file:filename_all()) -> ok. |
848 |
|
touch(File) -> |
849 |
2 |
{ok, Fd} = file:open(File, [append, binary, raw]), |
850 |
2 |
ok = file:close(Fd), |
851 |
2 |
ok = file:change_mode(File, 8#00600). |
852 |
|
|
853 |
|
%% Internal functions: run directory. |
854 |
|
|
855 |
|
-spec ensure_run_dir() -> ok. |
856 |
|
ensure_run_dir() -> |
857 |
1 |
RunDir = get_opt(run_dir), |
858 |
1 |
case filelib:ensure_dir(filename:join(RunDir, <<"file">>)) of |
859 |
|
ok -> |
860 |
1 |
?LOG_DEBUG("Using run directory ~ts", [RunDir]); |
861 |
|
{error, Reason} -> |
862 |
:-( |
exit({run_dir_failure, create, RunDir, Reason}) |
863 |
|
end. |
864 |
|
|
865 |
|
-spec clean_run_dir() -> ok. |
866 |
|
clean_run_dir() -> |
867 |
1 |
PEMFile = get_pem_file_path(), |
868 |
1 |
case filelib:is_regular(PEMFile) of |
869 |
|
true -> |
870 |
1 |
case file:delete(PEMFile) of |
871 |
|
ok -> |
872 |
1 |
?LOG_DEBUG("Removed ~ts", [PEMFile]); |
873 |
|
{error, Reason} -> |
874 |
:-( |
exit({run_dir_failure, clean, PEMFile, Reason}) |
875 |
|
end; |
876 |
|
false -> |
877 |
:-( |
?LOG_DEBUG("PEM file doesn't exist: ~ts", [PEMFile]) |
878 |
|
end. |
879 |
|
|
880 |
|
%% Internal functions: error message formatting. |
881 |
|
|
882 |
|
-spec format_error(atom() | tuple()) -> binary(). |
883 |
|
format_error({module_failure, Action, Mod, Reason}) -> |
884 |
:-( |
format("Failed to ~s ~s: ~p", [Action, Mod, Reason]); |
885 |
|
format_error({dependency_failure, Mod, Dep}) -> |
886 |
:-( |
format("Dependency ~s is missing; install it below ~s, or point ERL_LIBS " |
887 |
|
"to it, or disable ~s", [Dep, code:lib_dir(), Mod]); |
888 |
|
format_error({listener_failure, Action, IP, Port, Transport, Reason}) -> |
889 |
:-( |
format("Cannot ~s listening on ~s (~s): ~s", |
890 |
|
[Action, eturnal_misc:addr_to_str(IP, Port), Transport, |
891 |
|
inet:format_error(Reason)]); |
892 |
|
format_error({run_dir_failure, Action, RunDir, Reason}) -> |
893 |
:-( |
format("Cannot ~s run directory ~ts: ~ts", |
894 |
|
[Action, RunDir, file:format_error(Reason)]); |
895 |
|
format_error({pem_failure, File, Reason}) -> |
896 |
:-( |
format("Cannot create PEM file ~ts: ~ts", |
897 |
|
[File, file:format_error(Reason)]); |
898 |
|
format_error({otp_too_old, Key, Value, Vsn}) -> |
899 |
:-( |
format("Setting '~s: ~s' requires Erlang/OTP ~B or later", |
900 |
|
[Key, Value, Vsn]); |
901 |
|
format_error(proxy_config_failure) -> |
902 |
:-( |
<<"The 'proxy_protocol' ist not supported for 'udp'">>; |
903 |
|
format_error(turn_config_failure) -> |
904 |
:-( |
<<"The 'relay_max_port' must be larger than the 'relay_min_port'">>; |
905 |
|
format_error(_Unknown) -> |
906 |
:-( |
<<"Unknown error">>. |
907 |
|
|
908 |
|
-spec format(io:format(), [term()]) -> binary(). |
909 |
|
format(Fmt, Data) -> |
910 |
:-( |
case unicode:characters_to_binary(io_lib:format(Fmt, Data)) of |
911 |
|
Bin when is_binary(Bin) -> |
912 |
:-( |
Bin; |
913 |
|
{_, _, _} = Err -> |
914 |
:-( |
erlang:error(Err) |
915 |
|
end. |
916 |
|
|
917 |
|
%% EUnit tests. |
918 |
|
|
919 |
|
-ifdef(EUNIT). |
920 |
|
config_change_test_() -> |
921 |
4 |
[?_assert(logging_config_changed({[{log_level, info}], [], []})), |
922 |
1 |
?_assert(run_dir_config_changed({[{run_dir, <<"run">>}], [], []})), |
923 |
1 |
?_assert(relay_config_changed({[{relay_min_port, 50000}], [], []})), |
924 |
1 |
?_assert(listener_config_changed({[{max_bps, 42}], [], []})), |
925 |
1 |
?_assertNot(logging_config_changed({[{strict_expiry, false}], [], []})), |
926 |
1 |
?_assertNot(run_dir_config_changed({[{strict_expiry, false}], [], []})), |
927 |
1 |
?_assertNot(relay_config_changed({[{strict_expiry, false}], [], []})), |
928 |
1 |
?_assertNot(listener_config_changed({[{strict_expiry, false}], [], []}))]. |
929 |
|
-endif. |