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([hooks/1]). |
10 |
|
-export([supported_features/0]). |
11 |
|
-export([config_spec/0]). |
12 |
|
|
13 |
|
%% Hook handlers |
14 |
|
-export([get_key/3]). |
15 |
|
|
16 |
|
-export([process_key/1]). |
17 |
|
|
18 |
|
%% Public types |
19 |
|
-export_type([key/0, |
20 |
|
key_id/0, |
21 |
|
key_list/0, |
22 |
|
key_name/0, |
23 |
|
raw_key/0]). |
24 |
|
|
25 |
|
-include("mod_keystore.hrl"). |
26 |
|
-include("mongoose.hrl"). |
27 |
|
-include("mongoose_config_spec.hrl"). |
28 |
|
|
29 |
|
-define(DEFAULT_RAM_KEY_SIZE, 2048). |
30 |
|
|
31 |
|
%% A key name is used in the config file to name a key (a class of keys). |
32 |
|
%% The name doesn't differentiate between virtual hosts |
33 |
|
%% (i.e. there are multiple keys with the same name, |
34 |
|
%% one per each XMPP domain). |
35 |
|
-type key_name() :: atom(). |
36 |
|
%% A key ID is used to uniquely identify a key for storage backends. |
37 |
|
%% It's used to maintain separate instances of a key with the same name |
38 |
|
%% for different virtual hosts. |
39 |
|
-type key_id() :: {key_name(), mongooseim:host_type()}. |
40 |
|
-type raw_key() :: binary(). |
41 |
|
-type key_list() :: [{key_id(), raw_key()}]. |
42 |
|
-type key_type() :: ram | {file, file:name_all()}. |
43 |
|
|
44 |
|
-type key() :: #key{id :: key_id(), key :: raw_key()}. |
45 |
|
|
46 |
|
%% |
47 |
|
%% gen_mod callbacks |
48 |
|
%% |
49 |
|
|
50 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
51 |
|
start(HostType, Opts) -> |
52 |
2 |
ejabberd_sup:create_ets_table(keystore, [named_table, public, {read_concurrency, true}]), |
53 |
2 |
mod_keystore_backend:init(HostType, Opts), |
54 |
2 |
init_keys(HostType, Opts), |
55 |
2 |
ok. |
56 |
|
|
57 |
|
-spec stop(mongooseim:host_type()) -> ok. |
58 |
|
stop(HostType) -> |
59 |
2 |
clear_keystore_ets(HostType), |
60 |
2 |
mod_keystore_backend:stop(HostType), |
61 |
2 |
ok. |
62 |
|
|
63 |
|
-spec hooks(mongooseim:host_type()) -> gen_hook:hook_list(). |
64 |
|
hooks(HostType) -> |
65 |
4 |
[ |
66 |
|
{get_key, HostType, fun ?MODULE:get_key/3, #{}, 50} |
67 |
|
]. |
68 |
|
|
69 |
|
-spec supported_features() -> [atom()]. |
70 |
|
supported_features() -> |
71 |
2 |
[dynamic_domains]. |
72 |
|
|
73 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
74 |
|
config_spec() -> |
75 |
84 |
#section{ |
76 |
|
items = #{<<"ram_key_size">> => #option{type = integer, |
77 |
|
validate = non_negative}, |
78 |
|
<<"keys">> => #list{items = keys_spec(), |
79 |
|
format_items = map} |
80 |
|
}, |
81 |
|
defaults = #{<<"ram_key_size">> => ?DEFAULT_RAM_KEY_SIZE, |
82 |
|
<<"keys">> => #{}} |
83 |
|
}. |
84 |
|
|
85 |
|
keys_spec() -> |
86 |
84 |
#section{ |
87 |
|
items = #{<<"name">> => #option{type = atom, |
88 |
|
validate = non_empty}, |
89 |
|
<<"type">> => #option{type = atom, |
90 |
|
validate = {enum, [file, ram]}}, |
91 |
|
<<"path">> => #option{type = string, |
92 |
|
validate = filename} |
93 |
|
}, |
94 |
|
required = [<<"name">>, <<"type">>], |
95 |
|
process = fun ?MODULE:process_key/1 |
96 |
|
}. |
97 |
|
|
98 |
|
process_key(#{name := Name, type := file, path := Path}) -> |
99 |
:-( |
{Name, {file, Path}}; |
100 |
|
process_key(#{name := Name, type := ram}) -> |
101 |
:-( |
{Name, ram}. |
102 |
|
|
103 |
|
%% |
104 |
|
%% Hook handlers |
105 |
|
%% |
106 |
|
|
107 |
|
-spec get_key(HandlerAcc, Params, Extra) -> {ok, HandlerAcc} when |
108 |
|
HandlerAcc :: key_list(), |
109 |
|
Params :: #{key_id := key_id()}, |
110 |
|
Extra :: gen_hook:extra(). |
111 |
|
get_key(HandlerAcc, #{key_id := KeyID}, _) -> |
112 |
51 |
NewAcc = try |
113 |
|
%% This is OK, because the key is |
114 |
|
%% EITHER stored in ETS |
115 |
|
%% OR stored in BACKEND, |
116 |
|
%% with types of both stores returning |
117 |
|
%% AT MOST ONE value per key. |
118 |
51 |
(ets_get_key(KeyID) ++ |
119 |
|
mod_keystore_backend:get_key(KeyID) ++ |
120 |
|
HandlerAcc) |
121 |
|
catch |
122 |
|
E:R:S -> |
123 |
:-( |
?LOG_ERROR(#{what => get_key_failed, |
124 |
:-( |
error => E, reason => R, stacktrace => S}), |
125 |
:-( |
HandlerAcc |
126 |
|
end, |
127 |
51 |
{ok, NewAcc}. |
128 |
|
|
129 |
|
%% |
130 |
|
%% Internal functions |
131 |
|
%% |
132 |
|
clear_keystore_ets(HostType) -> |
133 |
2 |
Pattern = {{'_', HostType}, '$1'}, |
134 |
2 |
ets:match_delete(keystore, Pattern). |
135 |
|
|
136 |
|
init_keys(HostType, Opts = #{keys := Keys}) -> |
137 |
2 |
maps:map(fun(KeyName, KeyType) -> init_key({KeyName, KeyType}, HostType, Opts) end, Keys). |
138 |
|
|
139 |
|
-spec init_key({key_name(), key_type()}, mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
140 |
|
init_key({KeyName, {file, Path}}, HostType, _Opts) -> |
141 |
:-( |
{ok, Data} = file:read_file(Path), |
142 |
:-( |
true = ets_store_key({KeyName, HostType}, Data), |
143 |
:-( |
ok; |
144 |
|
init_key({KeyName, ram}, HostType, #{ram_key_size := KeySize}) -> |
145 |
4 |
ProposedKey = crypto:strong_rand_bytes(KeySize), |
146 |
4 |
KeyRecord = #key{id = {KeyName, HostType}, |
147 |
|
key = ProposedKey}, |
148 |
4 |
{ok, _ActualKey} = mod_keystore_backend:init_ram_key(HostType, KeyRecord), |
149 |
4 |
ok. |
150 |
|
|
151 |
|
%% It's easier to trace these than ets:{insert, lookup} - much less noise. |
152 |
|
ets_get_key(KeyID) -> |
153 |
51 |
ets:lookup(keystore, KeyID). |
154 |
|
|
155 |
|
ets_store_key(KeyID, RawKey) -> |
156 |
:-( |
ets:insert(keystore, {KeyID, RawKey}). |