| 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 | :-( |     ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), | 
| 742 | :-( |     LoggingKeys = [log_dir, | 
| 743 |  |                    log_level, | 
| 744 |  |                    log_rotate_size, | 
| 745 |  |                    log_rotate_count], | 
| 746 | :-( |     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 | :-( |     ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), | 
| 751 | :-( |     RunDirKeys = [run_dir], | 
| 752 | :-( |     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 | :-( |     ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), | 
| 757 | :-( |     RelayKeys = [relay_ipv4_addr, | 
| 758 |  |                  relay_ipv6_addr, | 
| 759 |  |                  relay_min_port, | 
| 760 |  |                  relay_max_port], | 
| 761 | :-( |     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 | :-( |     case relay_config_changed(ConfigChanges) or | 
| 766 |  |          run_dir_config_changed(ConfigChanges) of | 
| 767 |  |         true -> | 
| 768 | :-( |             true; | 
| 769 |  |         false -> | 
| 770 | :-( |             ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), | 
| 771 | :-( |             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 | :-( |             lists:any(fun(Key) -> | 
| 787 | :-( |                               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 | :-( |     [?_assert(logging_config_changed({[{log_level, info}], [], []})), | 
| 925 | :-( |      ?_assert(run_dir_config_changed({[{run_dir, <<"run">>}], [], []})), | 
| 926 | :-( |      ?_assert(relay_config_changed({[{relay_min_port, 50000}], [], []})), | 
| 927 | :-( |      ?_assert(listener_config_changed({[{max_bps, 42}], [], []})), | 
| 928 | :-( |      ?_assertNot(logging_config_changed({[{strict_expiry, false}], [], []})), | 
| 929 | :-( |      ?_assertNot(run_dir_config_changed({[{strict_expiry, false}], [], []})), | 
| 930 | :-( |      ?_assertNot(relay_config_changed({[{strict_expiry, false}], [], []})), | 
| 931 | :-( |      ?_assertNot(listener_config_changed({[{strict_expiry, false}], [], []}))]. | 
| 932 |  | -endif. |