./ct_report/coverage/mod_keystore.COVER.html

1 -module(mod_keystore).
2
3 -behaviour(gen_mod).
4 -behaviour(mongoose_module_metrics).
5
6 %% gen_mod callbacks
7 -export([start/2]).
8 -export([stop/1]).
9 -export([supported_features/0]).
10 -export([config_spec/0]).
11
12 %% Hook handlers
13 -export([get_key/2]).
14
15 -export([process_key/1]).
16
17 %% Public types
18 -export_type([key/0,
19 key_id/0,
20 key_list/0,
21 key_name/0,
22 raw_key/0]).
23
24 -ignore_xref([get_key/2]).
25
26 -include("mod_keystore.hrl").
27 -include("mongoose.hrl").
28 -include("mongoose_config_spec.hrl").
29
30 -define(DEFAULT_RAM_KEY_SIZE, 2048).
31
32 %% A key name is used in the config file to name a key (a class of keys).
33 %% The name doesn't differentiate between virtual hosts
34 %% (i.e. there are multiple keys with the same name,
35 %% one per each XMPP domain).
36 -type key_name() :: atom().
37 %% A key ID is used to uniquely identify a key for storage backends.
38 %% It's used to maintain separate instances of a key with the same name
39 %% for different virtual hosts.
40 -type key_id() :: {key_name(), mongooseim:host_type()}.
41 -type raw_key() :: binary().
42 -type key_list() :: [{key_id(), raw_key()}].
43 -type key_type() :: ram | {file, file:name_all()}.
44
45 -type key() :: #key{id :: key_id(), key :: raw_key()}.
46
47 %%
48 %% gen_mod callbacks
49 %%
50
51 -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
52 start(HostType, Opts) ->
53 1 create_keystore_ets(),
54 1 mod_keystore_backend:init(HostType, Opts),
55 1 init_keys(HostType, Opts),
56 1 ejabberd_hooks:add(hooks(HostType)),
57 1 ok.
58
59 -spec stop(mongooseim:host_type()) -> ok.
60 stop(HostType) ->
61 1 ejabberd_hooks:delete(hooks(HostType)),
62 1 clear_keystore_ets(HostType),
63 1 ok.
64
65 hooks(HostType) ->
66 2 [
67 {get_key, HostType, ?MODULE, get_key, 50}
68 ].
69
70 -spec supported_features() -> [atom()].
71 supported_features() ->
72 1 [dynamic_domains].
73
74 -spec config_spec() -> mongoose_config_spec:config_section().
75 config_spec() ->
76 152 #section{
77 items = #{<<"ram_key_size">> => #option{type = integer,
78 validate = non_negative},
79 <<"keys">> => #list{items = keys_spec(),
80 format_items = map}
81 },
82 defaults = #{<<"ram_key_size">> => ?DEFAULT_RAM_KEY_SIZE,
83 <<"keys">> => #{}},
84 format_items = map
85 }.
86
87 keys_spec() ->
88 152 #section{
89 items = #{<<"name">> => #option{type = atom,
90 validate = non_empty},
91 <<"type">> => #option{type = atom,
92 validate = {enum, [file, ram]}},
93 <<"path">> => #option{type = string,
94 validate = filename}
95 },
96 required = [<<"name">>, <<"type">>],
97 format_items = map,
98 process = fun ?MODULE:process_key/1
99 }.
100
101 process_key(#{name := Name, type := file, path := Path}) ->
102
:-(
{Name, {file, Path}};
103 process_key(#{name := Name, type := ram}) ->
104
:-(
{Name, ram}.
105
106 %%
107 %% Hook handlers
108 %%
109
110 -spec get_key(HandlerAcc, KeyID) -> Result when
111 HandlerAcc :: key_list(),
112 KeyID :: key_id(),
113 Result :: key_list().
114 get_key(HandlerAcc, KeyID) ->
115 25 try
116 %% This is OK, because the key is
117 %% EITHER stored in ETS
118 %% OR stored in BACKEND,
119 %% with types of both stores returning
120 %% AT MOST ONE value per key.
121 25 (ets_get_key(KeyID) ++
122 mod_keystore_backend:get_key(KeyID) ++
123 HandlerAcc)
124 catch
125 E:R:S ->
126
:-(
?LOG_ERROR(#{what => get_key_failed,
127
:-(
error => E, reason => R, stacktrace => S}),
128
:-(
HandlerAcc
129 end.
130
131 %%
132 %% Internal functions
133 %%
134
135 create_keystore_ets() ->
136 1 case does_table_exist(keystore) of
137
:-(
true -> ok;
138 false ->
139 1 BaseOpts = [named_table, public,
140 {read_concurrency, true}],
141 1 Opts = maybe_add_heir(whereis(ejabberd_sup), self(), BaseOpts),
142 1 ets:new(keystore, Opts),
143 1 ok
144 end.
145
146 %% In tests or when module is started in run-time, we need to set heir to the
147 %% ETS table, otherwise it will be destroy when the creator's process finishes.
148 %% When started normally during node start up, self() =:= EjdSupPid and there
149 %% is no need for setting heir
150 maybe_add_heir(EjdSupPid, EjdSupPid, BaseOpts) when is_pid(EjdSupPid) ->
151
:-(
BaseOpts;
152 maybe_add_heir(EjdSupPid, _Self, BaseOpts) when is_pid(EjdSupPid) ->
153 1 [{heir, EjdSupPid, testing} | BaseOpts];
154 maybe_add_heir(_, _, BaseOpts) ->
155
:-(
BaseOpts.
156
157 clear_keystore_ets(HostType) ->
158 1 Pattern = {{'_', HostType}, '$1'},
159 1 ets:match_delete(keystore, Pattern).
160
161 does_table_exist(NameOrTID) ->
162 1 ets:info(NameOrTID, name) /= undefined.
163
164 init_keys(HostType, Opts = #{keys := Keys}) ->
165 1 maps:map(fun(KeyName, KeyType) -> init_key({KeyName, KeyType}, HostType, Opts) end, Keys).
166
167 -spec init_key({key_name(), key_type()}, mongooseim:host_type(), gen_mod:module_opts()) -> ok.
168 init_key({KeyName, {file, Path}}, HostType, _Opts) ->
169
:-(
{ok, Data} = file:read_file(Path),
170
:-(
true = ets_store_key({KeyName, HostType}, Data),
171
:-(
ok;
172 init_key({KeyName, ram}, HostType, #{ram_key_size := KeySize}) ->
173 2 ProposedKey = crypto:strong_rand_bytes(KeySize),
174 2 KeyRecord = #key{id = {KeyName, HostType},
175 key = ProposedKey},
176 2 {ok, _ActualKey} = mod_keystore_backend:init_ram_key(HostType, KeyRecord),
177 2 ok.
178
179 %% It's easier to trace these than ets:{insert, lookup} - much less noise.
180 ets_get_key(KeyID) ->
181 25 ets:lookup(keystore, KeyID).
182
183 ets_store_key(KeyID, RawKey) ->
184
:-(
ets:insert(keystore, {KeyID, RawKey}).
Line Hits Source