./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([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 186 #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 186 #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}).
Line Hits Source