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. |