./ct_report/coverage/mongoose_subdomain_core.COVER.html

1 %% Generally, you should not call anything from this module.
2 %% Use mongoose_domain_api module instead.
3 -module(mongoose_subdomain_core).
4 -behaviour(gen_server).
5
6 -include("mongoose_logger.hrl").
7 %% API
8 -export([start/0, stop/0]).
9 -export([start_link/0]).
10
11 -export([register_subdomain/3,
12 unregister_subdomain/2,
13 add_domain/2,
14 remove_domain/2,
15 sync/0]).
16
17 -export([get_host_type/1,
18 get_subdomain_info/1,
19 get_all_subdomains_for_domain/1]).
20
21 %% gen_server callbacks
22 -export([init/1,
23 handle_call/3,
24 handle_cast/2,
25 handle_info/2,
26 code_change/3,
27 terminate/2]).
28
29 -ignore_xref([start_link/0, stop/0, sync/0]).
30
31 -ifdef(TEST).
32
33 -undef(LOG_ERROR).
34 -export([log_error/2]).
35 -define(LOG_ERROR(Error), ?MODULE:log_error(?FUNCTION_NAME, Error)).
36
37 -endif.
38
39 -define(SUBDOMAINS_TABLE, ?MODULE).
40 -define(REGISTRATION_TABLE, mongoose_subdomain_reg).
41
42 -type host_type() :: mongooseim:host_type().
43 -type domain() :: mongooseim:domain_name().
44 -type subdomain_pattern() :: mongoose_subdomain_utils:subdomain_pattern().
45 -type maybe_parent_domain() :: domain() | no_parent_domain.
46
47 -type reg_item() :: {{host_type(), subdomain_pattern()}, %% table key
48 Type :: fqdn | subdomain,
49 mongoose_packet_handler:t()}.
50
51 -record(subdomain_item, {host_type :: host_type() | '_',
52 subdomain :: domain() | '_', %% table key
53 subdomain_pattern :: subdomain_pattern() | '_',
54 parent_domain :: maybe_parent_domain() | '_',
55 packet_handler :: mongoose_packet_handler:t() | '_'}).
56
57 -type subdomain_item() :: #subdomain_item{}.
58
59 %% corresponds to the fields in #subdomain_item{} record
60 -type subdomain_info() :: #{host_type := host_type(),
61 subdomain := domain(),
62 subdomain_pattern := subdomain_pattern(),
63 parent_domain := maybe_parent_domain(),
64 packet_handler := mongoose_packet_handler:t()}.
65
66 -export_type([subdomain_info/0]).
67
68 %%--------------------------------------------------------------------
69 %% API
70 %%--------------------------------------------------------------------
71 -ifdef(TEST).
72
73 %% required for unit tests
74 start() ->
75 just_ok(gen_server:start({local, ?MODULE}, ?MODULE, [], [])).
76
77 stop() ->
78 gen_server:stop(?MODULE).
79
80 %% this interface is required only to detect errors in unit tests
81 log_error(_Function, _Error) -> ok.
82
83 -else.
84
85 start() ->
86 82 ChildSpec = {?MODULE, {?MODULE, start_link, []},
87 permanent, infinity, worker, [?MODULE]},
88 82 just_ok(supervisor:start_child(ejabberd_sup, ChildSpec)).
89
90 %% required for integration tests
91 stop() ->
92
:-(
supervisor:terminate_child(ejabberd_sup, ?MODULE),
93
:-(
supervisor:delete_child(ejabberd_sup, ?MODULE),
94
:-(
ok.
95
96 -endif.
97
98 start_link() ->
99 82 gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
100
101 -spec register_subdomain(host_type(), subdomain_pattern(),
102 mongoose_packet_handler:t()) ->
103 ok | {error, already_registered | subdomain_already_exists}.
104 register_subdomain(HostType, SubdomainPattern, PacketHandler) ->
105 430 NewPacketHandler = mongoose_packet_handler:add_extra(PacketHandler,
106 #{host_type => HostType}),
107 430 gen_server:call(?MODULE, {register, HostType, SubdomainPattern, NewPacketHandler}).
108
109 -spec unregister_subdomain(host_type(), subdomain_pattern()) -> ok.
110 unregister_subdomain(HostType, SubdomainPattern) ->
111 430 gen_server:call(?MODULE, {unregister, HostType, SubdomainPattern}).
112
113 -spec sync() -> ok.
114 sync() ->
115
:-(
gen_server:call(?MODULE, sync).
116
117 -spec add_domain(host_type(), domain()) -> ok.
118 add_domain(HostType, Domain) ->
119 14 gen_server:cast(?MODULE, {add_domain, HostType, Domain}).
120
121 -spec remove_domain(host_type(), domain()) -> ok.
122 remove_domain(HostType, Domain) ->
123 14 gen_server:cast(?MODULE, {remove_domain, HostType, Domain}).
124
125 -spec get_host_type(Subdomain :: domain()) -> {ok, host_type()} | {error, not_found}.
126 get_host_type(Subdomain) ->
127 3314 case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
128 [] ->
129 190 {error, not_found};
130 [#subdomain_item{host_type = HostType}] ->
131 3124 {ok, HostType}
132 end.
133
134 -spec get_subdomain_info(Subdomain :: domain()) -> {ok, subdomain_info()} | {error, not_found}.
135 get_subdomain_info(Subdomain) ->
136 1167 case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
137 [] ->
138 28 {error, not_found};
139 [Item] ->
140 1139 {ok, convert_subdomain_item_to_map(Item)}
141 end.
142
143 -spec get_all_subdomains_for_domain(Domain :: maybe_parent_domain()) -> [subdomain_info()].
144 %% if Domain param is set to no_parent_domain,
145 %% this function returns all the FQDN "subdomains".
146 get_all_subdomains_for_domain(Domain) ->
147 31 Pattern = #subdomain_item{parent_domain = Domain, _ = '_'},
148 31 Match = ets:match_object(?SUBDOMAINS_TABLE, Pattern),
149 31 [convert_subdomain_item_to_map(Item) || Item <- Match].
150
151 %%--------------------------------------------------------------------
152 %% gen_server callbacks
153 %%--------------------------------------------------------------------
154 init([]) ->
155 82 ets:new(?SUBDOMAINS_TABLE, [set, named_table, protected,
156 {keypos, #subdomain_item.subdomain},
157 {read_concurrency, true}]),
158 %% ?REGISTRATION_TABLE is protected only for traceability purposes
159 82 ets:new(?REGISTRATION_TABLE, [set, named_table, protected]),
160 %% no need for state, everything is kept in ETS tables
161 82 {ok, ok}.
162
163 handle_call({register, HostType, SubdomainPattern, PacketHandler}, From, State) ->
164 %% handle_register/4 must reply to the caller using gen_server:reply/2 interface
165 430 handle_register(HostType, SubdomainPattern, PacketHandler, From),
166 430 {noreply, State};
167 handle_call({unregister, HostType, SubdomainPattern}, _From, State) ->
168 430 Result = handle_unregister(HostType, SubdomainPattern),
169 430 {reply, Result, State};
170 handle_call(sync, _From, State) ->
171
:-(
{reply, ok, State};
172 handle_call(Request, From, State) ->
173
:-(
?UNEXPECTED_CALL(Request, From),
174
:-(
{reply, ok, State}.
175
176 handle_cast({add_domain, HostType, Domain}, State) ->
177 14 handle_add_domain(HostType, Domain),
178 14 {noreply, State};
179 handle_cast({remove_domain, HostType, Domain}, State) ->
180 14 handle_remove_domain(HostType, Domain),
181 14 {noreply, State};
182 handle_cast(Msg, State) ->
183
:-(
?UNEXPECTED_CAST(Msg),
184
:-(
{noreply, State}.
185
186 handle_info(Info, State) ->
187
:-(
?UNEXPECTED_INFO(Info),
188
:-(
{noreply, State}.
189
190 terminate(_Reason, _State) ->
191
:-(
ok.
192
193 code_change(_OldVsn, State, _Extra) ->
194
:-(
{ok, State}.
195
196 %%--------------------------------------------------------------------
197 %% local functions
198 %%--------------------------------------------------------------------
199 82 just_ok({ok, _}) -> ok;
200
:-(
just_ok(Other) -> Other.
201
202 -spec handle_register(host_type(), subdomain_pattern(),
203 mongoose_packet_handler:t(), any()) -> ok.
204 handle_register(HostType, SubdomainPattern, PacketHandler, From) ->
205 430 SubdomainType = mongoose_subdomain_utils:subdomain_type(SubdomainPattern),
206 430 RegItem = {{HostType, SubdomainPattern}, SubdomainType, PacketHandler},
207 430 case ets:insert_new(?REGISTRATION_TABLE, RegItem) of
208 true ->
209 430 case SubdomainType of
210 subdomain ->
211 430 Fn = fun(_HostType, Subdomain) ->
212 359 add_subdomain(RegItem, Subdomain)
213 end,
214 %% mongoose_domain_core:for_each_domain/2 can take quite long,
215 %% reply before running it.
216 430 gen_server:reply(From, ok),
217 430 mongoose_domain_core:for_each_domain(HostType, Fn);
218 fqdn ->
219
:-(
Result = add_subdomain(RegItem, no_parent_domain),
220
:-(
gen_server:reply(From, Result)
221 end;
222 false ->
223
:-(
gen_server:reply(From, {error, already_registered})
224 end.
225
226 -spec handle_unregister(host_type(), subdomain_pattern()) -> ok.
227 handle_unregister(HostType, SubdomainPattern) ->
228 430 Pattern = #subdomain_item{subdomain_pattern = SubdomainPattern,
229 host_type = HostType, _ = '_'},
230 430 Match = ets:match_object(?SUBDOMAINS_TABLE, Pattern),
231 430 remove_subdomains(Match),
232 430 ets:delete(?REGISTRATION_TABLE, {HostType, SubdomainPattern}),
233 430 ok.
234
235 -spec handle_add_domain(host_type(), domain()) -> ok.
236 handle_add_domain(HostType, Domain) ->
237 14 check_domain_name(HostType, Domain),
238 %% even if the domain name check fails, it's not easy to solve this
239 %% collision. so the best thing we can do is to report it and just keep
240 %% the data in both ETS tables (domains and subdomains) for further
241 %% troubleshooting.
242 14 Match = ets:match_object(?REGISTRATION_TABLE, {{HostType, '_'}, subdomain, '_'}),
243 14 add_subdomains(Match, Domain).
244
245 -spec handle_remove_domain(host_type(), domain()) -> ok.
246 handle_remove_domain(HostType, Domain) ->
247 14 Pattern = #subdomain_item{parent_domain = Domain, host_type = HostType, _ = '_'},
248 14 Match = ets:match_object(?SUBDOMAINS_TABLE, Pattern),
249 14 remove_subdomains(Match).
250
251 -spec remove_subdomains([subdomain_item()]) -> ok.
252 remove_subdomains(SubdomainItems) ->
253 444 Fn = fun(SubdomainItem) ->
254 365 remove_subdomain(SubdomainItem)
255 end,
256 444 lists:foreach(Fn, SubdomainItems).
257
258 -spec remove_subdomain(subdomain_item()) -> ok.
259 remove_subdomain(#subdomain_item{subdomain = Subdomain} = SubdomainItem) ->
260 365 ets:delete(?SUBDOMAINS_TABLE, Subdomain),
261 365 SubdomainInfo = convert_subdomain_item_to_map(SubdomainItem),
262 365 mongoose_lazy_routing:maybe_remove_subdomain(SubdomainInfo).
263
264 -spec add_subdomains([reg_item()], domain()) -> ok.
265 add_subdomains(RegItems, Domain) ->
266 14 Fn = fun(RegItem) ->
267 6 add_subdomain(RegItem, Domain)
268 end,
269 14 lists:foreach(Fn, RegItems).
270
271 -spec add_subdomain(reg_item(), maybe_parent_domain()) -> ok | {error, already_registered}.
272 add_subdomain(RegItem, Domain) ->
273 365 #subdomain_item{subdomain = Subdomain} = Item = make_subdomain_item(RegItem, Domain),
274 365 case ets:insert_new(?SUBDOMAINS_TABLE, Item) of
275 true ->
276 365 check_subdomain_name(Item),
277 %% even if the subdomain name check fails, it's not easy to solve this
278 %% collision. so the best thing we can do is to report it and just keep
279 %% the data in both ETS tables (domains and subdomains) for further
280 %% troubleshooting.
281 365 ok;
282 false ->
283
:-(
case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
284 [Item] ->
285
:-(
ok; %% exactly the same item is already inserted, it's fine.
286 [ExistingItem] ->
287
:-(
report_subdomains_collision(ExistingItem, Item),
288
:-(
{error, subdomain_already_exists}
289 end
290 end.
291
292 -spec make_subdomain_item(reg_item(), maybe_parent_domain()) -> subdomain_item().
293 make_subdomain_item({{HostType, SubdomainPattern}, Type, PacketHandler}, Domain) ->
294 365 Subdomain = case {Type, Domain} of
295 {fqdn, no_parent_domain} ->
296 %% not a subdomain, but FQDN
297
:-(
mongoose_subdomain_utils:get_fqdn(SubdomainPattern, <<"">>);
298 {subdomain, Domain} when is_binary(Domain) ->
299 365 mongoose_subdomain_utils:get_fqdn(SubdomainPattern, Domain)
300 end,
301 365 #subdomain_item{host_type = HostType, subdomain = Subdomain, parent_domain = Domain,
302 subdomain_pattern = SubdomainPattern, packet_handler = PacketHandler}.
303
304 -spec convert_subdomain_item_to_map(subdomain_item()) -> subdomain_info().
305 convert_subdomain_item_to_map(#subdomain_item{} = Item) ->
306 1541 Fields = record_info(fields, subdomain_item),
307 1541 [_ | Values] = tuple_to_list(Item),
308 1541 KVList = lists:zip(Fields, Values),
309 1541 maps:from_list(KVList).
310
311 -spec check_domain_name(mongooseim:host_type(), mongooseim:domain_name()) ->
312 boolean().
313 check_domain_name(_HostType, Domain) ->
314 14 case mongoose_subdomain_core:get_subdomain_info(Domain) of
315 14 {error, not_found} -> true;
316 {ok, SubdomainInfo} ->
317 %% TODO: this is critical collision, and it must be reported properly
318 %% think about adding some metric, so devops can set some alarm for it
319
:-(
?LOG_ERROR(#{what => check_domain_name_failed, domain => Domain}),
320 %% in case of domain/subdomain name conflicts, mongoose_lazy_routing
321 %% configures routing and IQ handling for a top level domain.
322 %% So to keep configuration consistent on all of the nodes in the cluster,
323 %% we must unregister subdomain and let mongoose_lazy_routing register top
324 %% level domain on the next routing.
325
:-(
mongoose_lazy_routing:maybe_remove_subdomain(SubdomainInfo),
326
:-(
false
327 end.
328
329 -spec check_subdomain_name(subdomain_item()) -> boolean().
330 check_subdomain_name(#subdomain_item{subdomain = Subdomain} = _SubdomainItem) ->
331 365 case mongoose_domain_core:get_host_type(Subdomain) of
332 365 {error, not_found} -> true;
333 {ok, _HostType} ->
334 %% TODO: this is critical collision, and it must be reported properly
335 %% think about adding some metric, so devops can set some alarm for it
336
:-(
?LOG_ERROR(#{what => check_subdomain_name_failed, subdomain => Subdomain}),
337
:-(
false
338 end.
339
340 -spec report_subdomains_collision(subdomain_item(), subdomain_item()) -> ok.
341 report_subdomains_collision(ExistingSubdomainItem, _NewSubdomainItem) ->
342
:-(
#subdomain_item{subdomain = Subdomain} = ExistingSubdomainItem,
343 %% TODO: this is critical collision, and it must be reported properly
344 %% think about adding some metric, so devops can set some alarm for it
345
:-(
?LOG_ERROR(#{what => subdomains_collision, subdomain => Subdomain}),
346
:-(
ok.
Line Hits Source