| 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 |  | %%% @doc An eturnal module adds functionality to the eturnal server. It is to be | 
| 20 |  | %%% named `mod_foo', where `foo' describes the added functionality. The module | 
| 21 |  | %%% may export `start/0', `stop/0', `handle_event/2', and `options/0' functions. | 
| 22 |  | %%% | 
| 23 |  | %%% If a `start/0' callback is exported, it must return `ok' or `{ok, Events}', | 
| 24 |  | %%% where `Events' is either a single {@link event()} or a list of {@link | 
| 25 |  | %%% events()} the module is interested in. Currently, the following events may | 
| 26 |  | %%% be triggered: `stun_query', `turn_session_start', `turn_session_stop', and | 
| 27 |  | %%% `protocol_error'. | 
| 28 |  | %%% | 
| 29 |  | %%% If a `start/0' function is exported and subscribes to one or more events, a | 
| 30 |  | %%% `handle_event/2' callback <em>must</em> be exported as well. It is called | 
| 31 |  | %%% with the {@link event()} name as the first argument and an {@link info()} | 
| 32 |  | %%% map with related data as the second. The contents of that map depend on the | 
| 33 |  | %%% event. Note that the `handle_event/2' function is executed in the context of | 
| 34 |  | %%% the process handling the STUN/TURN session, so it should never block. If it | 
| 35 |  | %%% might, and/or if it needs some state, one or more handler processes must be | 
| 36 |  | %%% created. | 
| 37 |  | %%% | 
| 38 |  | %%% The `options/0' callback returns an {@link options()} tuple with two | 
| 39 |  | %%% elements. The first is a map of module configuration options, where the keys | 
| 40 |  | %%% are the {@link option()} names and the values are functions that validate | 
| 41 |  | %%% the option values. Those functions are returned by the <a | 
| 42 |  | %%% href="https://hex.pm/packages/yval">yval</a> library, see the documentation | 
| 43 |  | %%% for the list of <a href="https://hexdocs.pm/yval/yval.html#index">available | 
| 44 |  | %%% validators</a>. The second element is a list of optional tuples to specify | 
| 45 |  | %%% any `{required, [Options]}' and/or `{defaults, #{Option => Value}}'. For | 
| 46 |  | %%% example: | 
| 47 |  | %%% | 
| 48 |  | %%% ``` | 
| 49 |  | %%% options() -> | 
| 50 |  | %%%     {#{threshold => yval:pos_int()}, | 
| 51 |  | %%%      [{defaults, | 
| 52 |  | %%%       #{threshold => 42}}]}. | 
| 53 |  | %%% ''' | 
| 54 |  | %%% | 
| 55 |  | %%% The option values are queried by calling {@link eturnal_module:get_opt/2} | 
| 56 |  | %%% with the module name as the first and the {@link option()} name as the | 
| 57 |  | %%% second argument. Note that the lookup is very efficient, so there's no point | 
| 58 |  | %%% in saving option values into some state. If the module has no configuration | 
| 59 |  | %%% options, the `options/0' function may be omitted. | 
| 60 |  | %%% | 
| 61 |  | %%% The optional `stop/0' callback must return `ok'. Note that the `start/0' and | 
| 62 |  | %%% `stop/0' functions might not just be called on eturnal startup and shutdown, | 
| 63 |  | %%% but also on configuration reloads. | 
| 64 |  | %%% | 
| 65 |  | %%% If the module depends on other applications, those must be added to the | 
| 66 |  | %%% `rebar.config' file, but not to the app file. They are to be started by | 
| 67 |  | %%% calling {@link eturnal_module:ensure_deps/2}, where the first argument is | 
| 68 |  | %%% the module name and the second is a list of dependency names. Note that | 
| 69 |  | %%% there's no need to list transitive dependencies. | 
| 70 |  | %%% | 
| 71 |  | %%% The module is enabled by adding its configuration to the `modules' section | 
| 72 |  | %%% of eturnal's configuration file as described in `doc/overview.edoc'. The | 
| 73 |  | %%% module configuration options are to be documented in that file as well. | 
| 74 |  |  | 
| 75 |  | -module(eturnal_module). | 
| 76 |  | -export([init/0, | 
| 77 |  |          terminate/0, | 
| 78 |  |          start/1, | 
| 79 |  |          stop/1, | 
| 80 |  |          handle_event/2, | 
| 81 |  |          options/1, | 
| 82 |  |          get_opt/2, | 
| 83 |  |          ensure_deps/2]). | 
| 84 |  | -export_type([dep/0, | 
| 85 |  |               event/0, | 
| 86 |  |               events/0, | 
| 87 |  |               info/0, | 
| 88 |  |               option/0, | 
| 89 |  |               options/0]). | 
| 90 |  |  | 
| 91 |  | -type dep() :: atom(). | 
| 92 |  | -type event() :: atom(). | 
| 93 |  | -type events() :: [event()]. | 
| 94 |  | -type info() :: #{atom() => term()}. | 
| 95 |  | -type option() :: atom(). | 
| 96 |  | -type options() :: {yval:validators(), [yval:validator_option()]}. | 
| 97 |  |  | 
| 98 |  | -callback start() -> ok | {ok, event() | [event()]}. | 
| 99 |  | -callback stop() -> ok. | 
| 100 |  | -callback handle_event(event(), info()) -> ok. | 
| 101 |  | -callback options() -> options(). | 
| 102 |  |  | 
| 103 |  | -optional_callbacks([start/0, stop/0, handle_event/2, options/0]). | 
| 104 |  |  | 
| 105 |  | -ifdef(EUNIT). | 
| 106 |  | -include_lib("eunit/include/eunit.hrl"). | 
| 107 |  | -endif. | 
| 108 |  | -include_lib("kernel/include/logger.hrl"). | 
| 109 |  | -ifdef(old_persistent_term). | 
| 110 |  | -define(m(Name), {m, Name}). | 
| 111 |  | -define(e(Name), {e, Name}). | 
| 112 |  | -else. | 
| 113 |  | -define(m(Name), {?MODULE, m, Name}). | 
| 114 |  | -define(e(Name), {?MODULE, e, Name}). | 
| 115 |  | -endif. | 
| 116 |  |  | 
| 117 |  | %% API. | 
| 118 |  |  | 
| 119 |  | -spec init() -> ok. | 
| 120 |  | -ifdef(old_persistent_term). | 
| 121 |  | init() -> | 
| 122 |  |     events = ets:new(events, [named_table, {read_concurrency, true}]), | 
| 123 |  |     ok. | 
| 124 |  | -else. | 
| 125 |  | init() -> | 
| 126 | 1 |     ok. | 
| 127 |  | -endif. | 
| 128 |  |  | 
| 129 |  | -spec terminate() -> ok. | 
| 130 |  | -ifdef(old_persistent_term). | 
| 131 |  | terminate() -> | 
| 132 |  |     true = ets:delete(events), | 
| 133 |  |     ok. | 
| 134 |  | -else. | 
| 135 |  | terminate() -> | 
| 136 | 1 |     ok. | 
| 137 |  | -endif. | 
| 138 |  |  | 
| 139 |  | -spec start(module()) -> ok | {error, term()}. | 
| 140 |  | start(Mod) -> | 
| 141 | 9 |     case erlang:function_exported(Mod, start, 0) of | 
| 142 |  |         true -> | 
| 143 | 9 |             ?LOG_DEBUG("Calling ~s:start/0", [Mod]), | 
| 144 | 9 |             try Mod:start() of | 
| 145 |  |                 ok -> | 
| 146 | :-( |                     ok; | 
| 147 |  |                 {ok, Events} -> | 
| 148 | 9 |                     ok = subscribe_events(Events, Mod) | 
| 149 |  |             catch _:Err:Stack -> | 
| 150 | :-( |                     ?LOG_DEBUG("Module ~s failed at starting:~n~p", | 
| 151 | :-( |                                [Mod, Stack]), | 
| 152 | :-( |                     {error, Err} | 
| 153 |  |             end; | 
| 154 |  |         false -> | 
| 155 | :-( |             ?LOG_DEBUG("Module ~s doesn't export start/0", [Mod]) | 
| 156 |  |     end. | 
| 157 |  |  | 
| 158 |  | -spec stop(module()) -> ok | {error, term()}. | 
| 159 |  | stop(Mod) -> | 
| 160 | 9 |     ok = unsubscribe_events(Mod), | 
| 161 | 9 |     case erlang:function_exported(Mod, stop, 0) of | 
| 162 |  |         true -> | 
| 163 | 9 |             ?LOG_DEBUG("Calling ~s:stop/1", [Mod]), | 
| 164 | 9 |             try ok = Mod:stop() | 
| 165 |  |             catch _:Err:Stack -> | 
| 166 | :-( |                     ?LOG_DEBUG("Module ~s failed at stopping:~n~p", | 
| 167 | :-( |                                [Mod, Stack]), | 
| 168 | :-( |                     {error, Err} | 
| 169 |  |             end; | 
| 170 |  |         false -> | 
| 171 | :-( |             ?LOG_DEBUG("Module ~s doesn't export stop/1", [Mod]), | 
| 172 | :-( |             ok | 
| 173 |  |     end. | 
| 174 |  |  | 
| 175 |  | -spec handle_event(event(), info()) -> ok. | 
| 176 |  | handle_event(Event, Info) -> | 
| 177 | 9 |     ?LOG_DEBUG("Got '~s' event", [Event]), | 
| 178 | 9 |     ok = lists:foreach( | 
| 179 |  |            fun(Mod) -> | 
| 180 | 21 |                    ?LOG_DEBUG("Calling ~s:handle_event/2", [Mod]), | 
| 181 | 21 |                    try ok = Mod:handle_event(Event, Info) | 
| 182 |  |                    catch _:_Err:Stack -> | 
| 183 | :-( |                            ?LOG_ERROR("Module ~s failed at handling '~s':~n~p", | 
| 184 | :-( |                                       [Mod, Event, Stack]) | 
| 185 |  |                    end | 
| 186 |  |            end, get_subscribers(Event)). | 
| 187 |  |  | 
| 188 |  | -spec options(module()) -> options(). | 
| 189 |  | options(Mod) -> | 
| 190 | 9 |     case erlang:function_exported(Mod, options, 0) of | 
| 191 |  |         true -> | 
| 192 | 9 |             ?LOG_DEBUG("Calling ~s:options/0", [Mod]), | 
| 193 | 9 |             Mod:options(); | 
| 194 |  |         false -> | 
| 195 | :-( |             ?LOG_DEBUG("Module ~s doesn't export options/1", [Mod]), | 
| 196 | :-( |             {#{}, []} | 
| 197 |  |     end. | 
| 198 |  |  | 
| 199 |  | -spec get_opt(module(), option()) -> term(). | 
| 200 |  | get_opt(Mod, Opt) -> | 
| 201 | 32 |     #{Mod := #{Opt := Val}} = eturnal:get_opt(modules), | 
| 202 | 32 |     Val. | 
| 203 |  |  | 
| 204 |  | -spec ensure_deps(module(), [dep()]) -> ok. | 
| 205 |  | ensure_deps(Mod, Deps) -> | 
| 206 | 6 |     lists:foreach(fun(Dep) -> ok = ensure_dep(Mod, Dep) end, Deps). | 
| 207 |  |  | 
| 208 |  | %% Internal functions. | 
| 209 |  |  | 
| 210 |  | -ifdef(old_persistent_term). | 
| 211 |  | -spec subscribe_events(event() | [event()], module()) -> ok. | 
| 212 |  | subscribe_events(Event, Mod) when is_atom(Event) -> | 
| 213 |  |     ok = subscribe_events([Event], Mod); | 
| 214 |  | subscribe_events(Events, Mod) -> | 
| 215 |  |     Entries = lists:map( | 
| 216 |  |                 fun(Event) -> | 
| 217 |  |                         case ets:lookup(events, ?e(Event)) of | 
| 218 |  |                             [] -> | 
| 219 |  |                                 {?e(Event), [Mod]}; | 
| 220 |  |                             [{_, Ms}] -> | 
| 221 |  |                                 {?e(Event), ordsets:add_element(Mod, Ms)} | 
| 222 |  |                         end | 
| 223 |  |                 end, Events), | 
| 224 |  |     true = ets:insert(events, [{?m(Mod), Events} | Entries]), | 
| 225 |  |     ok. | 
| 226 |  |  | 
| 227 |  | -spec unsubscribe_events(module()) -> ok. | 
| 228 |  | unsubscribe_events(Mod) -> | 
| 229 |  |     case ets:lookup(events, ?m(Mod)) of | 
| 230 |  |         [] -> | 
| 231 |  |             ok; | 
| 232 |  |         [{?m(Mod), Es}] -> | 
| 233 |  |             Entries = lists:map( | 
| 234 |  |                         fun(Event) -> | 
| 235 |  |                                 [{_, Ms}] = ets:lookup(events, ?e(Event)), | 
| 236 |  |                                 {?e(Event), ordsets:del_element(Mod, Ms)} | 
| 237 |  |                         end, Es), | 
| 238 |  |             true = ets:insert(events, Entries), | 
| 239 |  |             true = ets:delete(events, ?m(Mod)), | 
| 240 |  |             ok | 
| 241 |  |     end. | 
| 242 |  |  | 
| 243 |  | -spec get_subscribers(event()) -> [module()]. | 
| 244 |  | get_subscribers(Event) -> | 
| 245 |  |     case ets:lookup(events, ?e(Event)) of | 
| 246 |  |         [] -> | 
| 247 |  |             []; | 
| 248 |  |         [{_, Ms}] -> | 
| 249 |  |             Ms | 
| 250 |  |     end. | 
| 251 |  | -else. | 
| 252 |  | -spec subscribe_events(event() | [event()], module()) -> ok. | 
| 253 |  | subscribe_events(Event, Mod) when is_atom(Event) -> | 
| 254 | :-( |     ok = subscribe_events([Event], Mod); | 
| 255 |  | subscribe_events(Events, Mod) -> | 
| 256 | 9 |     ok = persistent_term:put(?m(Mod), Events), | 
| 257 | 9 |     ok = lists:foreach( | 
| 258 |  |            fun(Event) -> | 
| 259 | 21 |                    Ms = persistent_term:get(?e(Event), ordsets:new()), | 
| 260 | 21 |                    ok = persistent_term:put(?e(Event), | 
| 261 |  |                                             ordsets:add_element(Mod, Ms)) | 
| 262 |  |            end, Events). | 
| 263 |  |  | 
| 264 |  | -spec unsubscribe_events(module()) -> ok. | 
| 265 |  | unsubscribe_events(Mod) -> | 
| 266 | 9 |     Es = persistent_term:get(?m(Mod), []), | 
| 267 | 9 |     _R = persistent_term:erase(?m(Mod)), | 
| 268 | 9 |     ok = lists:foreach( | 
| 269 |  |            fun(Event) -> | 
| 270 | 21 |                    Ms = persistent_term:get(?e(Event)), | 
| 271 | 21 |                    ok = persistent_term:put(?e(Event), | 
| 272 |  |                                             ordsets:del_element(Mod, Ms)) | 
| 273 |  |            end, Es). | 
| 274 |  |  | 
| 275 |  | -spec get_subscribers(event()) -> [module()]. | 
| 276 |  | get_subscribers(Event) -> | 
| 277 | 9 |     persistent_term:get(?e(Event), []). | 
| 278 |  | -endif. | 
| 279 |  |  | 
| 280 |  | -spec ensure_dep(module(), dep()) -> ok. | 
| 281 |  | ensure_dep(Mod, Dep) -> | 
| 282 | 6 |     case application:ensure_all_started(Dep) of | 
| 283 |  |         {ok, _Apps} -> | 
| 284 | 6 |             ?LOG_DEBUG("Dependency ~s was available already", [Dep]), | 
| 285 | 6 |             ok; | 
| 286 |  |         {error, _Reason1} -> | 
| 287 | :-( |             ?LOG_DEBUG("Dependency ~s isn't started, loading it", [Dep]), | 
| 288 | :-( |             case start_app(Dep) of | 
| 289 |  |                 ok -> | 
| 290 | :-( |                     ?LOG_INFO("Dependency ~s is available", [Dep]), | 
| 291 | :-( |                     ok; | 
| 292 |  |                 {error, _Reason2} -> | 
| 293 | :-( |                     eturnal:abort({dependency_failure, Mod, Dep}) | 
| 294 |  |             end | 
| 295 |  |     end. | 
| 296 |  |  | 
| 297 |  | -spec start_app(dep()) -> ok | {error, term()}. | 
| 298 |  | start_app(App) -> | 
| 299 | :-( |     case load_app(App) of | 
| 300 |  |         ok -> | 
| 301 | :-( |             ?LOG_DEBUG("Loaded ~s, trying to start it", [App]), | 
| 302 | :-( |             case application:ensure_started(App) of | 
| 303 |  |                 ok -> | 
| 304 | :-( |                     ok; | 
| 305 |  |                 {error, {not_started, Dep}} -> | 
| 306 | :-( |                     ?LOG_DEBUG("~s depends on ~s, loading it", [App, Dep]), | 
| 307 | :-( |                     case start_app(Dep) of | 
| 308 |  |                         ok -> | 
| 309 | :-( |                             start_app(App); | 
| 310 |  |                         {error, _Reason} = Err -> | 
| 311 | :-( |                             Err | 
| 312 |  |                     end; | 
| 313 |  |                 {error, Reason} = Err -> | 
| 314 | :-( |                     ?LOG_DEBUG("Cannot start ~s: ~p", [App, Reason]), | 
| 315 | :-( |                     Err | 
| 316 |  |             end; | 
| 317 |  |         {error, Reason} = Err -> | 
| 318 | :-( |             ?LOG_DEBUG("Cannot load ~s: ~p", [App, Reason]), | 
| 319 | :-( |             Err | 
| 320 |  |     end. | 
| 321 |  |  | 
| 322 |  | -spec load_app(dep()) -> ok | {error, term()}. | 
| 323 |  | load_app(App) -> | 
| 324 | :-( |     try | 
| 325 | :-( |         LibDir = code:lib_dir(), | 
| 326 | :-( |         AppDir = lists:max(filelib:wildcard([App, "{,-*}"], LibDir)), | 
| 327 | :-( |         EbinDir = filename:join([LibDir, AppDir, "ebin"]), | 
| 328 | :-( |         AppFile = filename:join(EbinDir, [App, ".app"]), | 
| 329 | :-( |         {ok, [{application, App, Props}]} = file:consult(AppFile), | 
| 330 | :-( |         Mods = proplists:get_value(modules, Props), | 
| 331 | :-( |         true = code:add_path(EbinDir), | 
| 332 | :-( |         case lists:any(fun(Mod) -> | 
| 333 | :-( |                                code:module_status(Mod) =:= not_loaded | 
| 334 |  |                        end, Mods) of | 
| 335 |  |             true -> | 
| 336 | :-( |                 ?LOG_DEBUG("Loading modules: ~p", [Mods]), | 
| 337 | :-( |                 ok = code:atomic_load(Mods); | 
| 338 |  |             false -> | 
| 339 | :-( |                 ?LOG_DEBUG("Modules loaded already: ~p", [Mods]), | 
| 340 | :-( |                 ok | 
| 341 |  |         end | 
| 342 |  |     catch _:Err -> | 
| 343 | :-( |             {error, Err} | 
| 344 |  |     end. | 
| 345 |  |  | 
| 346 |  | %% EUnit tests. | 
| 347 |  |  | 
| 348 |  | -ifdef(EUNIT). | 
| 349 |  | load_test_() -> | 
| 350 | :-( |     [?_assertEqual(ok, start_app(eunit)), | 
| 351 | :-( |      ?_assertMatch({error, _}, start_app(nonexistent))]. | 
| 352 |  | -endif. |