| 1 |
|
%%% eturnal STUN/TURN server. |
| 2 |
|
%%% |
| 3 |
|
%%% Copyright (c) 2020-2025 Holger Weiss <holger@zedat.fu-berlin.de>. |
| 4 |
|
%%% Copyright (c) 2020-2025 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 |
|
% 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 |
| 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 |
| 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 |
?LOG_INFO("Reloading configuration"), |
| 629 |
2 |
try check_pem_file() of |
| 630 |
|
ok -> |
| 631 |
1 |
ok = fast_tls:clear_cache(), |
| 632 |
1 |
?LOG_INFO("Using new TLS certificate"); |
| 633 |
|
unchanged -> |
| 634 |
1 |
?LOG_DEBUG("TLS certificate unchanged") |
| 635 |
|
catch exit:Reason1 -> |
| 636 |
:-( |
?LOG_ERROR(format_error(Reason1)) |
| 637 |
|
end, |
| 638 |
2 |
try {stop_modules(State), start_modules()} of |
| 639 |
|
{ok, Modules} -> |
| 640 |
2 |
?LOG_DEBUG("Restarted modules"), |
| 641 |
2 |
{ok, State#eturnal_state{modules = Modules}} |
| 642 |
|
catch exit:Reason2 -> |
| 643 |
:-( |
?LOG_ERROR(format_error(Reason2)), |
| 644 |
:-( |
{ok, State} |
| 645 |
|
end; |
| 646 |
|
{error, Reason} = Err -> |
| 647 |
:-( |
?LOG_ERROR("Cannot reload configuration: ~ts", |
| 648 |
:-( |
[conf:format_error(Reason)]), |
| 649 |
:-( |
Err |
| 650 |
|
end. |
| 651 |
|
|
| 652 |
|
-spec apply_config_changes(state(), config_changes()) -> state(). |
| 653 |
|
apply_config_changes(State, {Changed, New, Removed} = ConfigChanges) -> |
| 654 |
:-( |
if length(Changed) > 0 -> |
| 655 |
:-( |
?LOG_DEBUG("Changed options: ~p", [Changed]); |
| 656 |
|
length(Changed) =:= 0 -> |
| 657 |
:-( |
?LOG_DEBUG("No changed options") |
| 658 |
|
end, |
| 659 |
:-( |
if length(Removed) > 0 -> |
| 660 |
:-( |
?LOG_DEBUG("Removed options: ~p", [Removed]); |
| 661 |
|
length(Removed) =:= 0 -> |
| 662 |
:-( |
?LOG_DEBUG("No removed options") |
| 663 |
|
end, |
| 664 |
:-( |
if length(New) > 0 -> |
| 665 |
:-( |
?LOG_DEBUG("New options: ~p", [New]); |
| 666 |
|
length(New) =:= 0 -> |
| 667 |
:-( |
?LOG_DEBUG("No new options") |
| 668 |
|
end, |
| 669 |
:-( |
try apply_logging_config_changes(ConfigChanges) |
| 670 |
|
catch exit:Reason1 -> |
| 671 |
:-( |
?LOG_ERROR(format_error(Reason1)) |
| 672 |
|
end, |
| 673 |
:-( |
try apply_run_dir_config_changes(ConfigChanges) |
| 674 |
|
catch exit:Reason2 -> |
| 675 |
:-( |
?LOG_ERROR(format_error(Reason2)) |
| 676 |
|
end, |
| 677 |
:-( |
try apply_relay_config_changes(ConfigChanges) |
| 678 |
|
catch exit:Reason3 -> |
| 679 |
:-( |
?LOG_ERROR(format_error(Reason3)) |
| 680 |
|
end, |
| 681 |
:-( |
try apply_listener_config_changes(ConfigChanges, State) |
| 682 |
|
catch exit:Reason4 -> |
| 683 |
:-( |
?LOG_ERROR(format_error(Reason4)), |
| 684 |
:-( |
State |
| 685 |
|
end. |
| 686 |
|
|
| 687 |
|
-spec apply_logging_config_changes(config_changes()) -> ok. |
| 688 |
|
apply_logging_config_changes(ConfigChanges) -> |
| 689 |
:-( |
case logging_config_changed(ConfigChanges) of |
| 690 |
|
true -> |
| 691 |
:-( |
?LOG_INFO("Using new logging configuration"), |
| 692 |
:-( |
ok = eturnal_logger:reconfigure(); |
| 693 |
|
false -> |
| 694 |
:-( |
?LOG_DEBUG("Logging configuration unchanged") |
| 695 |
|
end. |
| 696 |
|
|
| 697 |
|
-spec apply_run_dir_config_changes(config_changes()) -> ok. |
| 698 |
|
apply_run_dir_config_changes(ConfigChanges) -> |
| 699 |
:-( |
case run_dir_config_changed(ConfigChanges) of |
| 700 |
|
true -> |
| 701 |
:-( |
?LOG_INFO("Using new run directory configuration"), |
| 702 |
:-( |
ok = ensure_run_dir(), |
| 703 |
:-( |
case check_pem_file() of |
| 704 |
|
ok -> |
| 705 |
:-( |
ok = fast_tls:clear_cache(); |
| 706 |
|
unchanged -> |
| 707 |
:-( |
ok |
| 708 |
|
end; |
| 709 |
|
false -> |
| 710 |
:-( |
?LOG_DEBUG("Run directory configuration unchanged") |
| 711 |
|
end. |
| 712 |
|
|
| 713 |
|
-spec apply_relay_config_changes(config_changes()) -> ok. |
| 714 |
|
apply_relay_config_changes(ConfigChanges) -> |
| 715 |
:-( |
case relay_config_changed(ConfigChanges) of |
| 716 |
|
true -> |
| 717 |
:-( |
?LOG_INFO("Using new TURN relay configuration"), |
| 718 |
:-( |
ok = log_relay_addresses(); |
| 719 |
|
false -> |
| 720 |
:-( |
?LOG_DEBUG("TURN relay configuration unchanged") |
| 721 |
|
end. |
| 722 |
|
|
| 723 |
|
-spec apply_listener_config_changes(config_changes(), state()) -> state(). |
| 724 |
|
apply_listener_config_changes(ConfigChanges, State) -> |
| 725 |
:-( |
case listener_config_changed(ConfigChanges) of |
| 726 |
|
true -> |
| 727 |
:-( |
?LOG_INFO("Using new listener configuration"), |
| 728 |
:-( |
ok = check_turn_config(), |
| 729 |
:-( |
ok = check_proxy_config(), |
| 730 |
:-( |
ok = stop_listeners(State), |
| 731 |
:-( |
ok = timer:sleep(500), |
| 732 |
:-( |
Listeners = start_listeners(), |
| 733 |
:-( |
State#eturnal_state{listeners = Listeners}; |
| 734 |
|
false -> |
| 735 |
:-( |
?LOG_DEBUG("Listener configuration unchanged"), |
| 736 |
:-( |
State |
| 737 |
|
end. |
| 738 |
|
|
| 739 |
|
-spec logging_config_changed(config_changes()) -> boolean(). |
| 740 |
|
logging_config_changed({Changed, New, Removed}) -> |
| 741 |
2 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
| 742 |
2 |
LoggingKeys = [log_dir, |
| 743 |
|
log_level, |
| 744 |
|
log_rotate_size, |
| 745 |
|
log_rotate_count], |
| 746 |
2 |
lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, LoggingKeys). |
| 747 |
|
|
| 748 |
|
-spec run_dir_config_changed(config_changes()) -> boolean(). |
| 749 |
|
run_dir_config_changed({Changed, New, Removed}) -> |
| 750 |
4 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
| 751 |
4 |
RunDirKeys = [run_dir], |
| 752 |
4 |
lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, RunDirKeys). |
| 753 |
|
|
| 754 |
|
-spec relay_config_changed(config_changes()) -> boolean(). |
| 755 |
|
relay_config_changed({Changed, New, Removed}) -> |
| 756 |
4 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
| 757 |
4 |
RelayKeys = [relay_ipv4_addr, |
| 758 |
|
relay_ipv6_addr, |
| 759 |
|
relay_min_port, |
| 760 |
|
relay_max_port], |
| 761 |
4 |
lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, RelayKeys). |
| 762 |
|
|
| 763 |
|
-spec listener_config_changed(config_changes()) -> boolean(). |
| 764 |
|
listener_config_changed({Changed, New, Removed} = ConfigChanges) -> |
| 765 |
2 |
case relay_config_changed(ConfigChanges) or |
| 766 |
|
run_dir_config_changed(ConfigChanges) of |
| 767 |
|
true -> |
| 768 |
:-( |
true; |
| 769 |
|
false -> |
| 770 |
2 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
| 771 |
2 |
ListenerKeys = [listen, |
| 772 |
|
max_allocations, |
| 773 |
|
max_permissions, |
| 774 |
|
max_bps, |
| 775 |
|
blacklist, |
| 776 |
|
whitelist, |
| 777 |
|
blacklist_clients, |
| 778 |
|
whitelist_clients, |
| 779 |
|
blacklist_peers, |
| 780 |
|
whitelist_peers, |
| 781 |
|
realm, |
| 782 |
|
software_name, |
| 783 |
|
tls_options, |
| 784 |
|
tls_ciphers, |
| 785 |
|
tls_dh_file], |
| 786 |
2 |
lists:any(fun(Key) -> |
| 787 |
19 |
lists:member(Key, ModifiedKeys) |
| 788 |
|
end, ListenerKeys) |
| 789 |
|
end. |
| 790 |
|
|
| 791 |
|
%% Internal functions: PEM file handling. |
| 792 |
|
|
| 793 |
|
-spec get_pem_file_path() -> file:filename_all(). |
| 794 |
|
get_pem_file_path() -> |
| 795 |
6 |
filename:join(get_opt(run_dir), <<?PEM_FILE_NAME>>). |
| 796 |
|
|
| 797 |
|
-spec check_pem_file() -> ok | unchanged. |
| 798 |
|
check_pem_file() -> |
| 799 |
3 |
case tls_enabled() of |
| 800 |
|
true -> |
| 801 |
3 |
OutFile = get_pem_file_path(), |
| 802 |
3 |
case {get_opt(tls_crt_file), filelib:last_modified(OutFile)} of |
| 803 |
|
{none, OutTime} when OutTime =/= 0 -> |
| 804 |
1 |
?LOG_DEBUG("Keeping PEM file (~ts)", [OutFile]), |
| 805 |
1 |
unchanged; |
| 806 |
|
{none, OutTime} when OutTime =:= 0 -> |
| 807 |
2 |
?LOG_WARNING("TLS enabled without 'tls_crt_file', creating " |
| 808 |
:-( |
"self-signed certificate"), |
| 809 |
2 |
ok = create_self_signed(OutFile); |
| 810 |
|
{CrtFile, OutTime} -> |
| 811 |
:-( |
case filelib:last_modified(CrtFile) of |
| 812 |
|
CrtTime when CrtTime =< OutTime -> |
| 813 |
:-( |
?LOG_DEBUG("Keeping PEM file (~ts)", [OutFile]), |
| 814 |
:-( |
unchanged; |
| 815 |
|
CrtTime when CrtTime =/= 0 -> % Assert to be true. |
| 816 |
:-( |
?LOG_DEBUG("Updating PEM file (~ts)", [OutFile]), |
| 817 |
:-( |
ok = import_pem_file(CrtFile, OutFile) |
| 818 |
|
end |
| 819 |
|
end; |
| 820 |
|
false -> |
| 821 |
:-( |
?LOG_DEBUG("TLS not enabled, ignoring certificate configuration"), |
| 822 |
:-( |
unchanged |
| 823 |
|
end. |
| 824 |
|
|
| 825 |
|
-spec import_pem_file(binary(), file:filename_all()) -> ok. |
| 826 |
|
import_pem_file(CrtFile, OutFile) -> |
| 827 |
:-( |
try |
| 828 |
:-( |
ok = touch(OutFile), |
| 829 |
:-( |
case get_opt(tls_key_file) of |
| 830 |
|
KeyFile when is_binary(KeyFile) -> |
| 831 |
:-( |
ok = copy_file(KeyFile, OutFile, write); |
| 832 |
|
none -> |
| 833 |
:-( |
?LOG_INFO("No 'tls_key_file' specified, assuming key in ~ts", |
| 834 |
:-( |
[CrtFile]) |
| 835 |
|
end, |
| 836 |
:-( |
ok = copy_file(CrtFile, OutFile, append) |
| 837 |
|
catch error:{_, {error, Reason}} -> |
| 838 |
:-( |
exit({pem_failure, OutFile, Reason}) |
| 839 |
|
end. |
| 840 |
|
|
| 841 |
|
-spec copy_file(file:name_all(), file:name_all(), write | append) -> ok. |
| 842 |
|
copy_file(Src, Dst, Mode) -> |
| 843 |
:-( |
SrcMode = [read, binary, raw], |
| 844 |
:-( |
DstMode = [Mode, binary, raw], |
| 845 |
:-( |
{ok, _} = file:copy({Src, SrcMode}, {Dst, DstMode}), |
| 846 |
:-( |
?LOG_DEBUG("Copied ~ts into ~ts", [Src, Dst]). |
| 847 |
|
|
| 848 |
|
-spec touch(file:filename_all()) -> ok. |
| 849 |
|
touch(File) -> |
| 850 |
2 |
{ok, Fd} = file:open(File, [append, binary, raw]), |
| 851 |
2 |
ok = file:close(Fd), |
| 852 |
2 |
ok = file:change_mode(File, 8#00600). |
| 853 |
|
|
| 854 |
|
%% Internal functions: run directory. |
| 855 |
|
|
| 856 |
|
-spec ensure_run_dir() -> ok. |
| 857 |
|
ensure_run_dir() -> |
| 858 |
1 |
RunDir = get_opt(run_dir), |
| 859 |
1 |
case filelib:ensure_dir(filename:join(RunDir, <<"file">>)) of |
| 860 |
|
ok -> |
| 861 |
1 |
?LOG_DEBUG("Using run directory ~ts", [RunDir]); |
| 862 |
|
{error, Reason} -> |
| 863 |
:-( |
exit({run_dir_failure, create, RunDir, Reason}) |
| 864 |
|
end. |
| 865 |
|
|
| 866 |
|
-spec clean_run_dir() -> ok. |
| 867 |
|
clean_run_dir() -> |
| 868 |
1 |
PEMFile = get_pem_file_path(), |
| 869 |
1 |
case filelib:is_regular(PEMFile) of |
| 870 |
|
true -> |
| 871 |
1 |
case file:delete(PEMFile) of |
| 872 |
|
ok -> |
| 873 |
1 |
?LOG_DEBUG("Removed ~ts", [PEMFile]); |
| 874 |
|
{error, Reason} -> |
| 875 |
:-( |
exit({run_dir_failure, clean, PEMFile, Reason}) |
| 876 |
|
end; |
| 877 |
|
false -> |
| 878 |
:-( |
?LOG_DEBUG("PEM file doesn't exist: ~ts", [PEMFile]) |
| 879 |
|
end. |
| 880 |
|
|
| 881 |
|
%% Internal functions: error message formatting. |
| 882 |
|
|
| 883 |
|
-spec format_error(atom() | tuple()) -> binary(). |
| 884 |
|
format_error({module_failure, Action, Mod, Reason}) -> |
| 885 |
:-( |
format("Failed to ~s ~s: ~p", [Action, Mod, Reason]); |
| 886 |
|
format_error({dependency_failure, Mod, Dep}) -> |
| 887 |
:-( |
format("Dependency ~s is missing; install it below ~s, or point ERL_LIBS " |
| 888 |
|
"to it, or disable ~s", [Dep, code:lib_dir(), Mod]); |
| 889 |
|
format_error({listener_failure, Action, IP, Port, Transport, Reason}) -> |
| 890 |
:-( |
format("Cannot ~s listening on ~s (~s): ~s", |
| 891 |
|
[Action, eturnal_misc:addr_to_str(IP, Port), Transport, |
| 892 |
|
inet:format_error(Reason)]); |
| 893 |
|
format_error({run_dir_failure, Action, RunDir, Reason}) -> |
| 894 |
:-( |
format("Cannot ~s run directory ~ts: ~ts", |
| 895 |
|
[Action, RunDir, file:format_error(Reason)]); |
| 896 |
|
format_error({pem_failure, File, Reason}) when is_atom(Reason) -> |
| 897 |
:-( |
format("Cannot create PEM file ~ts: ~ts", |
| 898 |
|
[File, file:format_error(Reason)]); |
| 899 |
|
format_error({pem_failure, File, Reason}) -> |
| 900 |
:-( |
format("Cannot create PEM file ~ts: ~p", [File, Reason]); |
| 901 |
|
format_error({otp_too_old, Key, Value, Vsn}) -> |
| 902 |
:-( |
format("Setting '~s: ~s' requires Erlang/OTP ~B or later", |
| 903 |
|
[Key, Value, Vsn]); |
| 904 |
|
format_error(proxy_config_failure) -> |
| 905 |
:-( |
<<"The 'proxy_protocol' ist not supported for 'udp'">>; |
| 906 |
|
format_error(turn_config_failure) -> |
| 907 |
:-( |
<<"The 'relay_max_port' must be larger than the 'relay_min_port'">>; |
| 908 |
|
format_error(_Unknown) -> |
| 909 |
:-( |
<<"Unknown error">>. |
| 910 |
|
|
| 911 |
|
-spec format(io:format(), [term()]) -> binary(). |
| 912 |
|
format(Fmt, Data) -> |
| 913 |
:-( |
case unicode:characters_to_binary(io_lib:format(Fmt, Data)) of |
| 914 |
|
Bin when is_binary(Bin) -> |
| 915 |
:-( |
Bin; |
| 916 |
|
{_, _, _} = Err -> |
| 917 |
:-( |
erlang:error(Err) |
| 918 |
|
end. |
| 919 |
|
|
| 920 |
|
%% EUnit tests. |
| 921 |
|
|
| 922 |
|
-ifdef(EUNIT). |
| 923 |
|
config_change_test_() -> |
| 924 |
4 |
[?_assert(logging_config_changed({[{log_level, info}], [], []})), |
| 925 |
1 |
?_assert(run_dir_config_changed({[{run_dir, <<"run">>}], [], []})), |
| 926 |
1 |
?_assert(relay_config_changed({[{relay_min_port, 50000}], [], []})), |
| 927 |
1 |
?_assert(listener_config_changed({[{max_bps, 42}], [], []})), |
| 928 |
1 |
?_assertNot(logging_config_changed({[{strict_expiry, false}], [], []})), |
| 929 |
1 |
?_assertNot(run_dir_config_changed({[{strict_expiry, false}], [], []})), |
| 930 |
1 |
?_assertNot(relay_config_changed({[{strict_expiry, false}], [], []})), |
| 931 |
1 |
?_assertNot(listener_config_changed({[{strict_expiry, false}], [], []}))]. |
| 932 |
|
-endif. |