./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 83 ChildSpec = {?MODULE, {?MODULE, start_link, []},
87 permanent, infinity, worker, [?MODULE]},
88 83 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 83 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 443 NewPacketHandler = mongoose_packet_handler:add_extra(PacketHandler,
106 #{host_type => HostType}),
107 443 gen_server:call(?MODULE, {register, HostType, SubdomainPattern, NewPacketHandler}).
108
109 -spec unregister_subdomain(host_type(), subdomain_pattern()) -> ok.
110 unregister_subdomain(HostType, SubdomainPattern) ->
111 443 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 3920 case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
128 [] ->
129 195 {error, not_found};
130 [#subdomain_item{host_type = HostType}] ->
131 3725 {ok, HostType}
132 end.
133
134 -spec get_subdomain_info(Subdomain :: domain()) -> {ok, subdomain_info()} | {error, not_found}.
135 get_subdomain_info(Subdomain) ->
136 1382 case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
137 [] ->
138 28 {error, not_found};
139 [Item] ->
140 1354 {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 33 Pattern = #subdomain_item{parent_domain = Domain, _ = '_'},
148 33 Match = ets:match_object(?SUBDOMAINS_TABLE, Pattern),
149 33 [convert_subdomain_item_to_map(Item) || Item <- Match].
150
151 %%--------------------------------------------------------------------
152 %% gen_server callbacks
153 %%--------------------------------------------------------------------
154 init([]) ->
155 83 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 83 ets:new(?REGISTRATION_TABLE, [set, named_table, protected]),
160 %% no need for state, everything is kept in ETS tables
161 83 {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 443 handle_register(HostType, SubdomainPattern, PacketHandler, From),
166 443 {noreply, State};
167 handle_call({unregister, HostType, SubdomainPattern}, _From, State) ->
168 443 Result = handle_unregister(HostType, SubdomainPattern),
169 443 {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 83 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 443 SubdomainType = mongoose_subdomain_utils:subdomain_type(SubdomainPattern),
206 443 RegItem = {{HostType, SubdomainPattern}, SubdomainType, PacketHandler},
207 443 case ets:insert_new(?REGISTRATION_TABLE, RegItem) of
208 true ->
209 443 case SubdomainType of
210 subdomain ->
211 443 Fn = fun(_HostType, Subdomain) ->
212 371 add_subdomain(RegItem, Subdomain)
213 end,
214 %% mongoose_domain_core:for_each_domain/2 can take quite long,
215 %% reply before running it.
216 443 gen_server:reply(From, ok),
217 443 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 443 Pattern = #subdomain_item{subdomain_pattern = SubdomainPattern,
229 host_type = HostType, _ = '_'},
230 443 Match = ets:match_object(?SUBDOMAINS_TABLE, Pattern),
231 443 remove_subdomains(Match),
232 443 ets:delete(?REGISTRATION_TABLE, {HostType, SubdomainPattern}),
233 443 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 457 Fn = fun(SubdomainItem) ->
254 377 remove_subdomain(SubdomainItem)
255 end,
256 457 lists:foreach(Fn, SubdomainItems).
257
258 -spec remove_subdomain(subdomain_item()) -> ok.
259 remove_subdomain(#subdomain_item{subdomain = Subdomain} = SubdomainItem) ->
260 377 mongoose_hooks:unregister_subhost(Subdomain),
261 377 ets:delete(?SUBDOMAINS_TABLE, Subdomain),
262 377 SubdomainInfo = convert_subdomain_item_to_map(SubdomainItem),
263 377 mongoose_lazy_routing:maybe_remove_subdomain(SubdomainInfo).
264
265 -spec add_subdomains([reg_item()], domain()) -> ok.
266 add_subdomains(RegItems, Domain) ->
267 14 Fn = fun(RegItem) ->
268 6 add_subdomain(RegItem, Domain)
269 end,
270 14 lists:foreach(Fn, RegItems).
271
272 -spec add_subdomain(reg_item(), maybe_parent_domain()) -> ok | {error, already_registered}.
273 add_subdomain(RegItem, Domain) ->
274 377 #subdomain_item{subdomain = Subdomain} = Item = make_subdomain_item(RegItem, Domain),
275 377 case ets:insert_new(?SUBDOMAINS_TABLE, Item) of
276 true ->
277 377 mongoose_hooks:register_subhost(Subdomain, false),
278 377 check_subdomain_name(Item),
279 %% even if the subdomain name check fails, it's not easy to solve this
280 %% collision. so the best thing we can do is to report it and just keep
281 %% the data in both ETS tables (domains and subdomains) for further
282 %% troubleshooting.
283 377 ok;
284 false ->
285
:-(
case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
286 [Item] ->
287
:-(
ok; %% exactly the same item is already inserted, it's fine.
288 [ExistingItem] ->
289
:-(
report_subdomains_collision(ExistingItem, Item),
290
:-(
{error, subdomain_already_exists}
291 end
292 end.
293
294 -spec make_subdomain_item(reg_item(), maybe_parent_domain()) -> subdomain_item().
295 make_subdomain_item({{HostType, SubdomainPattern}, Type, PacketHandler}, Domain) ->
296 377 Subdomain = case {Type, Domain} of
297 {fqdn, no_parent_domain} ->
298 %% not a subdomain, but FQDN
299
:-(
mongoose_subdomain_utils:get_fqdn(SubdomainPattern, <<"">>);
300 {subdomain, Domain} when is_binary(Domain) ->
301 377 mongoose_subdomain_utils:get_fqdn(SubdomainPattern, Domain)
302 end,
303 377 #subdomain_item{host_type = HostType, subdomain = Subdomain, parent_domain = Domain,
304 subdomain_pattern = SubdomainPattern, packet_handler = PacketHandler}.
305
306 -spec convert_subdomain_item_to_map(subdomain_item()) -> subdomain_info().
307 convert_subdomain_item_to_map(#subdomain_item{} = Item) ->
308 1772 Fields = record_info(fields, subdomain_item),
309 1772 [_ | Values] = tuple_to_list(Item),
310 1772 KVList = lists:zip(Fields, Values),
311 1772 maps:from_list(KVList).
312
313 -spec check_domain_name(mongooseim:host_type(), mongooseim:domain_name()) ->
314 boolean().
315 check_domain_name(_HostType, Domain) ->
316 14 case mongoose_subdomain_core:get_subdomain_info(Domain) of
317 14 {error, not_found} -> true;
318 {ok, SubdomainInfo} ->
319 %% TODO: this is critical collision, and it must be reported properly
320 %% think about adding some metric, so devops can set some alarm for it
321
:-(
?LOG_ERROR(#{what => check_domain_name_failed, domain => Domain}),
322 %% in case of domain/subdomain name conflicts, mongoose_lazy_routing
323 %% configures routing and IQ handling for a top level domain.
324 %% So to keep configuration consistent on all of the nodes in the cluster,
325 %% we must unregister subdomain and let mongoose_lazy_routing register top
326 %% level domain on the next routing.
327
:-(
mongoose_lazy_routing:maybe_remove_subdomain(SubdomainInfo),
328
:-(
false
329 end.
330
331 -spec check_subdomain_name(subdomain_item()) -> boolean().
332 check_subdomain_name(#subdomain_item{subdomain = Subdomain} = _SubdomainItem) ->
333 377 case mongoose_domain_core:get_host_type(Subdomain) of
334 377 {error, not_found} -> true;
335 {ok, _HostType} ->
336 %% TODO: this is critical collision, and it must be reported properly
337 %% think about adding some metric, so devops can set some alarm for it
338
:-(
?LOG_ERROR(#{what => check_subdomain_name_failed, subdomain => Subdomain}),
339
:-(
false
340 end.
341
342 -spec report_subdomains_collision(subdomain_item(), subdomain_item()) -> ok.
343 report_subdomains_collision(ExistingSubdomainItem, _NewSubdomainItem) ->
344
:-(
#subdomain_item{subdomain = Subdomain} = ExistingSubdomainItem,
345 %% TODO: this is critical collision, and it must be reported properly
346 %% think about adding some metric, so devops can set some alarm for it
347
:-(
?LOG_ERROR(#{what => subdomains_collision, subdomain => Subdomain}),
348
:-(
ok.
Line Hits Source