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