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 |
164 |
#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 |
164 |
#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). |