1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : ejabberd_router.erl |
3 |
|
%%% Author : Alexey Shchepin <alexey@process-one.net> |
4 |
|
%%% Purpose : Main router |
5 |
|
%%% Created : 27 Nov 2002 by Alexey Shchepin <alexey@process-one.net> |
6 |
|
%%% |
7 |
|
%%% |
8 |
|
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne |
9 |
|
%%% |
10 |
|
%%% This program is free software; you can redistribute it and/or |
11 |
|
%%% modify it under the terms of the GNU General Public License as |
12 |
|
%%% published by the Free Software Foundation; either version 2 of the |
13 |
|
%%% License, or (at your option) any later version. |
14 |
|
%%% |
15 |
|
%%% This program is distributed in the hope that it will be useful, |
16 |
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 |
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
18 |
|
%%% General Public License for more details. |
19 |
|
%%% |
20 |
|
%%% You should have received a copy of the GNU General Public License |
21 |
|
%%% along with this program; if not, write to the Free Software |
22 |
|
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
23 |
|
%%% |
24 |
|
%%%---------------------------------------------------------------------- |
25 |
|
|
26 |
|
-module(ejabberd_router). |
27 |
|
-author('alexey@process-one.net'). |
28 |
|
|
29 |
|
-behaviour(gen_server). |
30 |
|
%% API |
31 |
|
-export([route/3, |
32 |
|
route/4, |
33 |
|
route_error/4, |
34 |
|
route_error_reply/4, |
35 |
|
is_component_dirty/1, |
36 |
|
dirty_get_all_components/1, |
37 |
|
register_components/2, |
38 |
|
register_components/3, |
39 |
|
register_components/4, |
40 |
|
register_component/2, |
41 |
|
register_component/3, |
42 |
|
register_component/4, |
43 |
|
lookup_component/1, |
44 |
|
lookup_component/2, |
45 |
|
unregister_component/1, |
46 |
|
unregister_component/2, |
47 |
|
unregister_components/1, |
48 |
|
unregister_components/2 |
49 |
|
]). |
50 |
|
|
51 |
|
-export([start_link/0]). |
52 |
|
-export([routes_cleanup_on_nodedown/2]). |
53 |
|
|
54 |
|
%% gen_server callbacks |
55 |
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). |
56 |
|
|
57 |
|
%% debug exports for tests |
58 |
|
-export([update_tables/0]). |
59 |
|
|
60 |
|
-ignore_xref([register_component/2, register_component/3, register_component/4, |
61 |
|
register_components/2, register_components/3, |
62 |
|
route_error/4, routes_cleanup_on_nodedown/2, start_link/0, |
63 |
|
unregister_component/1, unregister_component/2, unregister_components/2, |
64 |
|
unregister_routes/1, update_tables/0]). |
65 |
|
|
66 |
|
-include("mongoose.hrl"). |
67 |
|
-include("jlib.hrl"). |
68 |
|
-include("external_component.hrl"). |
69 |
|
|
70 |
|
-record(state, {}). |
71 |
|
|
72 |
|
-type domain() :: binary(). |
73 |
|
|
74 |
|
-type external_component() :: #external_component{domain :: domain(), |
75 |
|
handler :: mongoose_packet_handler:t(), |
76 |
|
is_hidden :: boolean()}. |
77 |
|
|
78 |
|
% Not simple boolean() because is probably going to support third value in the future: only_hidden. |
79 |
|
% Besides, it increases readability. |
80 |
|
-type return_hidden() :: only_public | all. |
81 |
|
|
82 |
|
%%==================================================================== |
83 |
|
%% API |
84 |
|
%%==================================================================== |
85 |
|
|
86 |
|
-spec start_link() -> 'ignore' | {'error', _} | {'ok', pid()}. |
87 |
|
start_link() -> |
88 |
80 |
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). |
89 |
|
|
90 |
|
%% @doc The main routing function. It puts the message through a chain |
91 |
|
%% of filtering/routing modules, as defined in config 'routing_modules' |
92 |
|
%% setting (default is hardcoded in default_routing_modules function |
93 |
|
%% of this module). Each of those modules should use xmpp_router behaviour |
94 |
|
%% and implement two functions: |
95 |
|
%% filter/3 - should return either 'drop' atom or its args |
96 |
|
%% route/3, which should either: |
97 |
|
%% - deliver the message locally by calling mongoose_local_delivery:do_route/5 |
98 |
|
%% and return 'done' |
99 |
|
%% - deliver the message it its own way and return 'done' |
100 |
|
%% - return its args |
101 |
|
%% - return a tuple of {From, To, Packet} which might be modified |
102 |
|
%% For both functions, returning a 'drop' or 'done' atom terminates the procedure, |
103 |
|
%% while returning a tuple means 'proceed' and the tuple is passed to |
104 |
|
%% the next module in sequence. |
105 |
|
-spec route(From :: jid:jid(), |
106 |
|
To :: jid:jid(), |
107 |
|
Packet :: mongoose_acc:t()|exml:element()) -> mongoose_acc:t(). |
108 |
|
route(From, To, #xmlel{} = Packet) -> |
109 |
|
% ?LOG_ERROR("Deprecated - it should be Acc: ~p", [Packet]), |
110 |
1903 |
Acc = mongoose_acc:new(#{ location => ?LOCATION, |
111 |
|
lserver => From#jid.lserver, |
112 |
|
element => Packet, |
113 |
|
from_jid => From, |
114 |
|
to_jid => To }), |
115 |
|
% (called by broadcasting) |
116 |
1903 |
route(From, To, Acc); |
117 |
|
route(From, To, Acc) -> |
118 |
4885 |
?LOG_DEBUG(#{what => route, acc => Acc}), |
119 |
4885 |
El = mongoose_acc:element(Acc), |
120 |
4885 |
RoutingModules = routing_modules_list(), |
121 |
4885 |
NewAcc = route(From, To, Acc, El, RoutingModules), |
122 |
4883 |
?LOG_DEBUG(#{what => routing_result, |
123 |
|
routing_result => mongoose_acc:get(router, result, {drop, undefined}, NewAcc), |
124 |
4883 |
routing_modules => RoutingModules, acc => Acc}), |
125 |
4883 |
NewAcc. |
126 |
|
|
127 |
|
-spec route(From :: jid:jid(), |
128 |
|
To :: jid:jid(), |
129 |
|
Acc :: mongoose_acc:t(), |
130 |
|
El :: exml:element() | {error, term()}) -> mongoose_acc:t(). |
131 |
|
route(_From, _To, Acc, {error, Reason} = Err) -> |
132 |
:-( |
?LOG_INFO(#{what => cannot_route_stanza, acc => Acc, reason => Reason}), |
133 |
:-( |
mongoose_acc:append(router, result, Err, Acc); |
134 |
|
route(From, To, Acc, El) -> |
135 |
12108 |
Acc1 = mongoose_acc:update_stanza(#{ from_jid => From, |
136 |
|
to_jid => To, |
137 |
|
element => El }, Acc), |
138 |
12108 |
?LOG_DEBUG(#{what => route, acc => Acc1}), |
139 |
12108 |
RoutingModules = routing_modules_list(), |
140 |
12108 |
NewAcc = route(From, To, Acc1, El, RoutingModules), |
141 |
12108 |
?LOG_DEBUG(#{what => routing_result, |
142 |
|
routing_result => mongoose_acc:get(router, result, {drop, undefined}, NewAcc), |
143 |
12108 |
routing_modules => RoutingModules, acc => Acc}), |
144 |
12108 |
NewAcc. |
145 |
|
|
146 |
|
%% Route the error packet only if the originating packet is not an error itself. |
147 |
|
%% RFC3920 9.3.1 |
148 |
|
-spec route_error(From :: jid:jid(), |
149 |
|
To :: jid:jid(), |
150 |
|
Acc :: mongoose_acc:t(), |
151 |
|
ErrPacket :: exml:element()) -> mongoose_acc:t(). |
152 |
|
route_error(From, To, Acc, ErrPacket) -> |
153 |
:-( |
case mongoose_acc:stanza_type(Acc) of |
154 |
|
<<"error">> -> |
155 |
:-( |
Acc; |
156 |
|
_ -> |
157 |
:-( |
route(From, To, Acc, ErrPacket) |
158 |
|
end. |
159 |
|
|
160 |
|
-spec route_error_reply(jid:jid(), jid:jid(), mongoose_acc:t(), exml:element()) -> |
161 |
|
mongoose_acc:t(). |
162 |
|
route_error_reply(From, To, Acc, Error) -> |
163 |
:-( |
{Acc1, ErrorReply} = jlib:make_error_reply(Acc, Error), |
164 |
:-( |
route_error(From, To, Acc1, ErrorReply). |
165 |
|
|
166 |
|
|
167 |
|
-spec register_components([Domain :: domain()], |
168 |
|
Handler :: mongoose_packet_handler:t()) -> ok | {error, any()}. |
169 |
|
register_components(Domains, Handler) -> |
170 |
:-( |
register_components(Domains, node(), Handler). |
171 |
|
|
172 |
|
-spec register_components([Domain :: domain()], |
173 |
|
Node :: node(), |
174 |
|
Handler :: mongoose_packet_handler:t()) -> ok | {error, any()}. |
175 |
|
register_components(Domains, Node, Handler) -> |
176 |
:-( |
register_components(Domains, Node, Handler, false). |
177 |
|
|
178 |
|
-spec register_components([Domain :: domain()], |
179 |
|
Node :: node(), |
180 |
|
Handler :: mongoose_packet_handler:t(), |
181 |
|
AreHidden :: boolean()) -> ok | {error, any()}. |
182 |
|
register_components(Domains, Node, Handler, AreHidden) -> |
183 |
42 |
LDomains = [{jid:nameprep(Domain), Domain} || Domain <- Domains], |
184 |
42 |
F = fun() -> |
185 |
42 |
[do_register_component(LDomain, Handler, Node, AreHidden) || LDomain <- LDomains], |
186 |
36 |
ok |
187 |
|
end, |
188 |
42 |
case mnesia:transaction(F) of |
189 |
36 |
{atomic, ok} -> ok; |
190 |
6 |
{aborted, Reason} -> {error, Reason} |
191 |
|
end. |
192 |
|
|
193 |
|
%% @doc |
194 |
|
%% components are registered in two places: external_components table as local components |
195 |
|
%% and external_components_global as globals. Registration should be done by register_component/1 |
196 |
|
%% or register_components/1, which registers them for current node; the arity 2 funcs are |
197 |
|
%% here for testing. |
198 |
|
-spec register_component(Domain :: domain(), |
199 |
|
Handler :: mongoose_packet_handler:t()) -> ok | {error, any()}. |
200 |
|
register_component(Domain, Handler) -> |
201 |
:-( |
register_component(Domain, node(), Handler). |
202 |
|
|
203 |
|
-spec register_component(Domain :: domain(), |
204 |
|
Node :: node(), |
205 |
|
Handler :: mongoose_packet_handler:t()) -> ok | {error, any()}. |
206 |
|
register_component(Domain, Node, Handler) -> |
207 |
:-( |
register_component(Domain, Node, Handler, false). |
208 |
|
|
209 |
|
-spec register_component(Domain :: domain(), |
210 |
|
Node :: node(), |
211 |
|
Handler :: mongoose_packet_handler:t(), |
212 |
|
IsHidden :: boolean()) -> ok | {error, any()}. |
213 |
|
register_component(Domain, Node, Handler, IsHidden) -> |
214 |
:-( |
register_components([Domain], Node, Handler, IsHidden). |
215 |
|
|
216 |
|
do_register_component({error, Domain}, _Handler, _Node, _IsHidden) -> |
217 |
:-( |
error({invalid_domain, Domain}); |
218 |
|
do_register_component({LDomain, _}, Handler, Node, IsHidden) -> |
219 |
43 |
case check_component(LDomain, Node) of |
220 |
|
ok -> |
221 |
37 |
ComponentGlobal = #external_component{domain = LDomain, handler = Handler, |
222 |
|
node = Node, is_hidden = IsHidden}, |
223 |
37 |
mnesia:write(external_component_global, ComponentGlobal, write), |
224 |
37 |
NDomain = {LDomain, Node}, |
225 |
37 |
Component = #external_component{domain = NDomain, handler = Handler, |
226 |
|
node = Node, is_hidden = IsHidden}, |
227 |
37 |
mnesia:write(Component), |
228 |
37 |
mongoose_hooks:register_subhost(LDomain, IsHidden); |
229 |
6 |
_ -> mnesia:abort(route_already_exists) |
230 |
|
end. |
231 |
|
|
232 |
|
%% @doc Check if the component/route is already registered somewhere; ok means it is not, so we are |
233 |
|
%% ok to proceed, anything else means the domain/node pair is already serviced. |
234 |
|
%% true and false are there because that's how orelse works. |
235 |
|
-spec check_component(binary(), Node :: node()) -> ok | error. |
236 |
|
check_component(LDomain, Node) -> |
237 |
43 |
case check_dynamic_domains(LDomain) |
238 |
41 |
orelse check_component_route(LDomain) |
239 |
41 |
orelse check_component_local(LDomain, Node) |
240 |
37 |
orelse check_component_global(LDomain, Node) of |
241 |
6 |
true -> error; |
242 |
37 |
false -> ok |
243 |
|
end. |
244 |
|
|
245 |
|
check_dynamic_domains(LDomain)-> |
246 |
43 |
{error, not_found} =/= mongoose_domain_api:get_host_type(LDomain). |
247 |
|
|
248 |
|
%% check that route for this domain is not already registered |
249 |
|
check_component_route(LDomain) -> |
250 |
41 |
no_route =/= mongoose_router:lookup_route(LDomain). |
251 |
|
|
252 |
|
%% check that there is no local component for domain:node pair |
253 |
|
check_component_local(LDomain, Node) -> |
254 |
41 |
NDomain = {LDomain, Node}, |
255 |
41 |
[] =/= mnesia:read(external_component, NDomain). |
256 |
|
|
257 |
|
%% check that there is no component registered globally for this node |
258 |
|
check_component_global(LDomain, Node) -> |
259 |
37 |
undefined =/= get_global_component(LDomain, Node). |
260 |
|
|
261 |
|
%% Find a component registered globally for this node (internal use) |
262 |
|
get_global_component([], _) -> |
263 |
37 |
undefined; |
264 |
|
get_global_component([Comp|Tail], Node) -> |
265 |
40 |
case Comp of |
266 |
|
#external_component{node = Node} -> |
267 |
38 |
Comp; |
268 |
|
_ -> |
269 |
2 |
get_global_component(Tail, Node) |
270 |
|
end; |
271 |
|
get_global_component(LDomain, Node) -> |
272 |
75 |
get_global_component(mnesia:read(external_component_global, LDomain), Node). |
273 |
|
|
274 |
|
|
275 |
|
-spec unregister_components([Domains :: domain()]) -> {atomic, ok}. |
276 |
|
unregister_components(Domains) -> |
277 |
36 |
unregister_components(Domains, node()). |
278 |
|
-spec unregister_components([Domains :: domain()], Node :: node()) -> {atomic, ok}. |
279 |
|
unregister_components(Domains, Node) -> |
280 |
36 |
LDomains = [{jid:nameprep(Domain), Domain} || Domain <- Domains], |
281 |
36 |
F = fun() -> |
282 |
37 |
[do_unregister_component(LDomain, Node) || LDomain <- LDomains], |
283 |
36 |
ok |
284 |
|
end, |
285 |
36 |
{atomic, ok} = mnesia:transaction(F). |
286 |
|
|
287 |
|
do_unregister_component({error, Domain}, _Node) -> |
288 |
:-( |
error({invalid_domain, Domain}); |
289 |
|
do_unregister_component({LDomain, _}, Node) -> |
290 |
38 |
case get_global_component(LDomain, Node) of |
291 |
|
undefined -> |
292 |
:-( |
ok; |
293 |
|
Comp -> |
294 |
38 |
ok = mnesia:delete_object(external_component_global, Comp, write) |
295 |
|
end, |
296 |
37 |
ok = mnesia:delete({external_component, {LDomain, Node}}), |
297 |
37 |
mongoose_hooks:unregister_subhost(LDomain), |
298 |
37 |
ok. |
299 |
|
|
300 |
|
-spec unregister_component(Domain :: domain()) -> {atomic, ok}. |
301 |
|
unregister_component(Domain) -> |
302 |
:-( |
unregister_components([Domain]). |
303 |
|
|
304 |
|
-spec unregister_component(Domain :: domain(), Node :: node()) -> {atomic, ok}. |
305 |
|
unregister_component(Domain, Node) -> |
306 |
:-( |
unregister_components([Domain], Node). |
307 |
|
|
308 |
|
%% @doc Returns a list of components registered for this domain by any node, |
309 |
|
%% the choice is yours. |
310 |
|
-spec lookup_component(Domain :: jid:lserver()) -> [external_component()]. |
311 |
|
lookup_component(Domain) -> |
312 |
278 |
mnesia:dirty_read(external_component_global, Domain). |
313 |
|
|
314 |
|
%% @doc Returns a list of components registered for this domain at the given node. |
315 |
|
%% (must be only one, or nothing) |
316 |
|
-spec lookup_component(Domain :: jid:lserver(), Node :: node()) -> [external_component()]. |
317 |
|
lookup_component(Domain, Node) -> |
318 |
249 |
mnesia:dirty_read(external_component, {Domain, Node}). |
319 |
|
|
320 |
|
-spec dirty_get_all_components(return_hidden()) -> [jid:lserver()]. |
321 |
|
dirty_get_all_components(all) -> |
322 |
44 |
mnesia:dirty_all_keys(external_component_global); |
323 |
|
dirty_get_all_components(only_public) -> |
324 |
27 |
MatchNonHidden = {#external_component{ domain = '$1', is_hidden = false, _ = '_' }, [], ['$1']}, |
325 |
27 |
mnesia:dirty_select(external_component_global, [MatchNonHidden]). |
326 |
|
|
327 |
|
-spec is_component_dirty(jid:lserver()) -> boolean(). |
328 |
|
is_component_dirty(Domain) -> |
329 |
:-( |
[] =/= lookup_component(Domain). |
330 |
|
|
331 |
|
%%==================================================================== |
332 |
|
%% gen_server callbacks |
333 |
|
%%==================================================================== |
334 |
|
|
335 |
|
init([]) -> |
336 |
80 |
update_tables(), |
337 |
|
|
338 |
|
%% add distributed service_component routes |
339 |
80 |
mnesia:create_table(external_component, |
340 |
|
[{attributes, record_info(fields, external_component)}, |
341 |
|
{local_content, true}]), |
342 |
80 |
mnesia:add_table_copy(external_component, node(), ram_copies), |
343 |
80 |
mnesia:create_table(external_component_global, |
344 |
|
[{attributes, record_info(fields, external_component)}, |
345 |
|
{type, bag}, |
346 |
|
{record_name, external_component}]), |
347 |
80 |
mnesia:add_table_copy(external_component_global, node(), ram_copies), |
348 |
80 |
mongoose_metrics:ensure_metric(global, routingErrors, spiral), |
349 |
80 |
ejabberd_hooks:add(node_cleanup, global, ?MODULE, routes_cleanup_on_nodedown, 90), |
350 |
|
|
351 |
80 |
{ok, #state{}}. |
352 |
|
|
353 |
|
handle_call(_Request, _From, State) -> |
354 |
:-( |
Reply = ok, |
355 |
:-( |
{reply, Reply, State}. |
356 |
|
|
357 |
|
handle_cast(_Msg, State) -> |
358 |
:-( |
{noreply, State}. |
359 |
|
|
360 |
|
handle_info(_Info, State) -> |
361 |
:-( |
{noreply, State}. |
362 |
|
|
363 |
|
terminate(_Reason, _State) -> |
364 |
:-( |
ejabberd_hooks:delete(node_cleanup, global, ?MODULE, routes_cleanup_on_nodedown, 90), |
365 |
:-( |
ok. |
366 |
|
|
367 |
|
code_change(_OldVsn, State, _Extra) -> |
368 |
:-( |
{ok, State}. |
369 |
|
|
370 |
|
%%-------------------------------------------------------------------- |
371 |
|
%%% Internal functions |
372 |
|
%%-------------------------------------------------------------------- |
373 |
|
routing_modules_list() -> |
374 |
16993 |
mongoose_config:get_opt(routing_modules). |
375 |
|
|
376 |
|
-spec route(From :: jid:jid(), |
377 |
|
To :: jid:jid(), |
378 |
|
Acc :: mongoose_acc:t(), |
379 |
|
Packet :: exml:element(), |
380 |
|
[atom()]) -> mongoose_acc:t(). |
381 |
|
route(_From, _To, Acc, _Packet, []) -> |
382 |
:-( |
?LOG_ERROR(#{what => no_more_routing_modules, acc => Acc}), |
383 |
:-( |
mongoose_metrics:update(global, routingErrors, 1), |
384 |
:-( |
mongoose_acc:append(router, result, {error, out_of_modules}, Acc); |
385 |
|
route(OrigFrom, OrigTo, Acc0, OrigPacket, [M|Tail]) -> |
386 |
34387 |
try xmpp_router:call_filter(M, OrigFrom, OrigTo, Acc0, OrigPacket) of |
387 |
|
drop -> |
388 |
370 |
mongoose_acc:append(router, result, {drop, M}, Acc0); |
389 |
|
{OrigFrom, OrigTo, Acc1, OrigPacketFiltered} -> |
390 |
34015 |
try xmpp_router:call_route(M, OrigFrom, OrigTo, Acc1, OrigPacketFiltered) of |
391 |
|
{done, Acc2} -> |
392 |
16612 |
mongoose_acc:append(router, result, {done, M}, Acc2); |
393 |
|
{From, To, NAcc1, Packet} -> |
394 |
17394 |
route(From, To, NAcc1, Packet, Tail) |
395 |
|
catch Class:Reason:Stacktrace -> |
396 |
9 |
?LOG_WARNING(#{what => routing_failed, |
397 |
|
router_module => M, acc => Acc1, |
398 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}), |
399 |
9 |
mongoose_acc:append(router, result, {error, {M, Reason}}, Acc1) |
400 |
|
end |
401 |
|
catch Class:Reason:Stacktrace -> |
402 |
:-( |
?LOG_WARNING(#{what => route_filter_failed, |
403 |
|
text => <<"Error when filtering packet in router">>, |
404 |
|
router_module => M, acc => Acc0, |
405 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}), |
406 |
:-( |
mongoose_acc:append(router, result, {error, {M, Reason}}, Acc0) |
407 |
|
end. |
408 |
|
|
409 |
|
update_tables() -> |
410 |
80 |
case catch mnesia:table_info(external_component, attributes) of |
411 |
|
[domain, handler, node] -> |
412 |
:-( |
mnesia:delete_table(external_component); |
413 |
|
[domain, handler, node, is_hidden] -> |
414 |
53 |
ok; |
415 |
|
{'EXIT', _} -> |
416 |
27 |
ok |
417 |
|
end, |
418 |
80 |
case catch mnesia:table_info(external_component_global, attributes) of |
419 |
|
[domain, handler, node] -> |
420 |
:-( |
UpdateFun = fun({external_component, Domain, Handler, Node}) -> |
421 |
:-( |
{external_component, Domain, Handler, Node, false} |
422 |
|
end, |
423 |
:-( |
mnesia:transform_table(external_component_global, UpdateFun, |
424 |
|
[domain, handler, node, is_hidden]); |
425 |
|
[domain, handler, node, is_hidden] -> |
426 |
53 |
ok; |
427 |
|
{'EXIT', _} -> |
428 |
27 |
ok |
429 |
|
end. |
430 |
|
|
431 |
|
-spec routes_cleanup_on_nodedown(map(), node()) -> map(). |
432 |
|
routes_cleanup_on_nodedown(Acc, Node) -> |
433 |
:-( |
Entries = mnesia:dirty_match_object(external_component_global, |
434 |
|
#external_component{node = Node, _ = '_'}), |
435 |
:-( |
[mnesia:dirty_delete_object(external_component_global, Entry) || Entry <- Entries], |
436 |
:-( |
maps:put(?MODULE, ok, Acc). |