./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
8 %% API
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, 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 %% this interface is required only to detect errors in unit tests
73 log_error(_Function, _Error) -> ok.
74 -endif.
75
76 start_link() ->
77 104 gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
78
79 -spec register_subdomain(host_type(), subdomain_pattern(),
80 mongoose_packet_handler:t()) ->
81 ok | {error, already_registered | subdomain_already_exists}.
82 register_subdomain(HostType, SubdomainPattern, PacketHandler) ->
83 599 NewPacketHandler = mongoose_packet_handler:add_extra(PacketHandler,
84 #{host_type => HostType}),
85 599 gen_server:call(?MODULE, {register, HostType, SubdomainPattern, NewPacketHandler}).
86
87 -spec unregister_subdomain(host_type(), subdomain_pattern()) -> ok.
88 unregister_subdomain(HostType, SubdomainPattern) ->
89 601 gen_server:call(?MODULE, {unregister, HostType, SubdomainPattern}).
90
91 -spec sync() -> ok.
92 sync() ->
93
:-(
gen_server:call(?MODULE, sync).
94
95 -spec add_domain(host_type(), domain()) -> ok.
96 add_domain(HostType, Domain) ->
97 166 gen_server:cast(?MODULE, {add_domain, HostType, Domain}).
98
99 -spec remove_domain(host_type(), domain()) -> ok.
100 remove_domain(HostType, Domain) ->
101 78 gen_server:cast(?MODULE, {remove_domain, HostType, Domain}).
102
103 -spec get_host_type(Subdomain :: domain()) -> {ok, host_type()} | {error, not_found}.
104 get_host_type(Subdomain) ->
105 11401 case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
106 [] ->
107 463 {error, not_found};
108 [#subdomain_item{host_type = HostType}] ->
109 10938 {ok, HostType}
110 end.
111
112 -spec get_subdomain_info(Subdomain :: domain()) -> {ok, subdomain_info()} | {error, not_found}.
113 get_subdomain_info(Subdomain) ->
114 3838 case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
115 [] ->
116 307 {error, not_found};
117 [Item] ->
118 3531 {ok, convert_subdomain_item_to_map(Item)}
119 end.
120
121 -spec get_all_subdomains_for_domain(Domain :: maybe_parent_domain()) -> [subdomain_info()].
122 %% if Domain param is set to no_parent_domain,
123 %% this function returns all the FQDN "subdomains".
124 get_all_subdomains_for_domain(Domain) ->
125 43 Pattern = #subdomain_item{parent_domain = Domain, _ = '_'},
126 43 Match = ets:match_object(?SUBDOMAINS_TABLE, Pattern),
127 43 [convert_subdomain_item_to_map(Item) || Item <- Match].
128
129 %%--------------------------------------------------------------------
130 %% gen_server callbacks
131 %%--------------------------------------------------------------------
132 init([]) ->
133 104 ets:new(?SUBDOMAINS_TABLE, [set, named_table, protected,
134 {keypos, #subdomain_item.subdomain},
135 {read_concurrency, true}]),
136 %% ?REGISTRATION_TABLE is protected only for traceability purposes
137 104 ets:new(?REGISTRATION_TABLE, [set, named_table, protected]),
138 %% no need for state, everything is kept in ETS tables
139 104 {ok, ok}.
140
141 handle_call({register, HostType, SubdomainPattern, PacketHandler}, From, State) ->
142 %% handle_register/4 must reply to the caller using gen_server:reply/2 interface
143 599 handle_register(HostType, SubdomainPattern, PacketHandler, From),
144 599 {noreply, State};
145 handle_call({unregister, HostType, SubdomainPattern}, _From, State) ->
146 601 Result = handle_unregister(HostType, SubdomainPattern),
147 601 {reply, Result, State};
148 handle_call(sync, _From, State) ->
149
:-(
{reply, ok, State};
150 handle_call(Request, From, State) ->
151
:-(
?UNEXPECTED_CALL(Request, From),
152
:-(
{reply, ok, State}.
153
154 handle_cast({add_domain, HostType, Domain}, State) ->
155 166 handle_add_domain(HostType, Domain),
156 166 {noreply, State};
157 handle_cast({remove_domain, HostType, Domain}, State) ->
158 78 handle_remove_domain(HostType, Domain),
159 78 {noreply, State};
160 handle_cast(Msg, State) ->
161
:-(
?UNEXPECTED_CAST(Msg),
162
:-(
{noreply, State}.
163
164 handle_info(Info, State) ->
165
:-(
?UNEXPECTED_INFO(Info),
166
:-(
{noreply, State}.
167
168 terminate(_Reason, _State) ->
169
:-(
ok.
170
171 code_change(_OldVsn, State, _Extra) ->
172
:-(
{ok, State}.
173
174 %%--------------------------------------------------------------------
175 %% local functions
176 %%--------------------------------------------------------------------
177
178 -spec handle_register(host_type(), subdomain_pattern(),
179 mongoose_packet_handler:t(), any()) -> ok.
180 handle_register(HostType, SubdomainPattern, PacketHandler, From) ->
181 599 SubdomainType = mongoose_subdomain_utils:subdomain_type(SubdomainPattern),
182 599 RegItem = {{HostType, SubdomainPattern}, SubdomainType, PacketHandler},
183 599 case ets:insert_new(?REGISTRATION_TABLE, RegItem) of
184 true ->
185 599 case SubdomainType of
186 subdomain ->
187 599 Fn = fun(_HostType, Subdomain) ->
188 499 add_subdomain(RegItem, Subdomain)
189 end,
190 %% mongoose_domain_core:for_each_domain/2 can take quite long,
191 %% reply before running it.
192 599 gen_server:reply(From, ok),
193 599 mongoose_domain_core:for_each_domain(HostType, Fn);
194 fqdn ->
195
:-(
Result = add_subdomain(RegItem, no_parent_domain),
196
:-(
gen_server:reply(From, Result)
197 end;
198 false ->
199
:-(
gen_server:reply(From, {error, already_registered})
200 end.
201
202 -spec handle_unregister(host_type(), subdomain_pattern()) -> ok.
203 handle_unregister(HostType, SubdomainPattern) ->
204 601 Pattern = #subdomain_item{subdomain_pattern = SubdomainPattern,
205 host_type = HostType, _ = '_'},
206 601 Match = ets:match_object(?SUBDOMAINS_TABLE, Pattern),
207 601 remove_subdomains(Match),
208 601 ets:delete(?REGISTRATION_TABLE, {HostType, SubdomainPattern}),
209 601 ok.
210
211 -spec handle_add_domain(host_type(), domain()) -> ok.
212 handle_add_domain(HostType, Domain) ->
213 166 check_domain_name(HostType, Domain),
214 %% even if the domain name check fails, it's not easy to solve this
215 %% collision. so the best thing we can do is to report it and just keep
216 %% the data in both ETS tables (domains and subdomains) for further
217 %% troubleshooting.
218 166 Match = ets:match_object(?REGISTRATION_TABLE, {{HostType, '_'}, subdomain, '_'}),
219 166 add_subdomains(Match, Domain).
220
221 -spec handle_remove_domain(host_type(), domain()) -> ok.
222 handle_remove_domain(HostType, Domain) ->
223 78 Pattern = #subdomain_item{parent_domain = Domain, host_type = HostType, _ = '_'},
224 78 Match = ets:match_object(?SUBDOMAINS_TABLE, Pattern),
225 78 remove_subdomains(Match).
226
227 -spec remove_subdomains([subdomain_item()]) -> ok.
228 remove_subdomains(SubdomainItems) ->
229 679 Fn = fun(SubdomainItem) ->
230 507 remove_subdomain(SubdomainItem)
231 end,
232 679 lists:foreach(Fn, SubdomainItems).
233
234 -spec remove_subdomain(subdomain_item()) -> ok.
235 remove_subdomain(#subdomain_item{subdomain = Subdomain} = SubdomainItem) ->
236 507 mongoose_hooks:unregister_subhost(Subdomain),
237 507 ets:delete(?SUBDOMAINS_TABLE, Subdomain),
238 507 SubdomainInfo = convert_subdomain_item_to_map(SubdomainItem),
239 507 mongoose_lazy_routing:maybe_remove_subdomain(SubdomainInfo).
240
241 -spec add_subdomains([reg_item()], domain()) -> ok.
242 add_subdomains(RegItems, Domain) ->
243 166 Fn = fun(RegItem) ->
244 6 add_subdomain(RegItem, Domain)
245 end,
246 166 lists:foreach(Fn, RegItems).
247
248 -spec add_subdomain(reg_item(), maybe_parent_domain()) -> ok | {error, already_registered}.
249 add_subdomain(RegItem, Domain) ->
250 505 #subdomain_item{subdomain = Subdomain} = Item = make_subdomain_item(RegItem, Domain),
251 505 case ets:insert_new(?SUBDOMAINS_TABLE, Item) of
252 true ->
253 505 mongoose_hooks:register_subhost(Subdomain, false),
254 505 check_subdomain_name(Item),
255 %% even if the subdomain name check fails, it's not easy to solve this
256 %% collision. so the best thing we can do is to report it and just keep
257 %% the data in both ETS tables (domains and subdomains) for further
258 %% troubleshooting.
259 505 ok;
260 false ->
261
:-(
case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
262 [Item] ->
263
:-(
ok; %% exactly the same item is already inserted, it's fine.
264 [ExistingItem] ->
265
:-(
report_subdomains_collision(ExistingItem, Item),
266
:-(
{error, subdomain_already_exists}
267 end
268 end.
269
270 -spec make_subdomain_item(reg_item(), maybe_parent_domain()) -> subdomain_item().
271 make_subdomain_item({{HostType, SubdomainPattern}, Type, PacketHandler}, Domain) ->
272 505 Subdomain = case {Type, Domain} of
273 {fqdn, no_parent_domain} ->
274 %% not a subdomain, but FQDN
275
:-(
mongoose_subdomain_utils:get_fqdn(SubdomainPattern, <<"">>);
276 {subdomain, Domain} when is_binary(Domain) ->
277 505 mongoose_subdomain_utils:get_fqdn(SubdomainPattern, Domain)
278 end,
279 505 #subdomain_item{host_type = HostType, subdomain = Subdomain, parent_domain = Domain,
280 subdomain_pattern = SubdomainPattern, packet_handler = PacketHandler}.
281
282 -spec convert_subdomain_item_to_map(subdomain_item()) -> subdomain_info().
283 convert_subdomain_item_to_map(#subdomain_item{} = Item) ->
284 4099 Fields = record_info(fields, subdomain_item),
285 4099 [_ | Values] = tuple_to_list(Item),
286 4099 KVList = lists:zip(Fields, Values),
287 4099 maps:from_list(KVList).
288
289 -spec check_domain_name(mongooseim:host_type(), mongooseim:domain_name()) ->
290 boolean().
291 check_domain_name(_HostType, Domain) ->
292 166 case mongoose_subdomain_core:get_subdomain_info(Domain) of
293 166 {error, not_found} -> true;
294 {ok, SubdomainInfo} ->
295 %% TODO: this is critical collision, and it must be reported properly
296 %% think about adding some metric, so devops can set some alarm for it
297
:-(
?LOG_ERROR(#{what => check_domain_name_failed, domain => Domain}),
298 %% in case of domain/subdomain name conflicts, mongoose_lazy_routing
299 %% configures routing and IQ handling for a top level domain.
300 %% So to keep configuration consistent on all of the nodes in the cluster,
301 %% we must unregister subdomain and let mongoose_lazy_routing register top
302 %% level domain on the next routing.
303
:-(
mongoose_lazy_routing:maybe_remove_subdomain(SubdomainInfo),
304
:-(
false
305 end.
306
307 -spec check_subdomain_name(subdomain_item()) -> boolean().
308 check_subdomain_name(#subdomain_item{subdomain = Subdomain} = _SubdomainItem) ->
309 505 case mongoose_domain_core:get_host_type(Subdomain) of
310 505 {error, not_found} -> true;
311 {ok, _HostType} ->
312 %% TODO: this is critical collision, and it must be reported properly
313 %% think about adding some metric, so devops can set some alarm for it
314
:-(
?LOG_ERROR(#{what => check_subdomain_name_failed, subdomain => Subdomain}),
315
:-(
false
316 end.
317
318 -spec report_subdomains_collision(subdomain_item(), subdomain_item()) -> ok.
319 report_subdomains_collision(ExistingSubdomainItem, _NewSubdomainItem) ->
320
:-(
#subdomain_item{subdomain = Subdomain} = ExistingSubdomainItem,
321 %% TODO: this is critical collision, and it must be reported properly
322 %% think about adding some metric, so devops can set some alarm for it
323
:-(
?LOG_ERROR(#{what => subdomains_collision, subdomain => Subdomain}),
324
:-(
ok.
Line Hits Source