1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : ejabberd_local.erl |
3 |
|
%%% Author : Alexey Shchepin <alexey@process-one.net> |
4 |
|
%%% Purpose : Route local packets |
5 |
|
%%% Created : 30 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 |
|
%%%---------------------------------------------------------------------- |
27 |
|
%%% FIXME: the code in this module uses Host term to identify domain |
28 |
|
%%% name, not a host type. |
29 |
|
%%%---------------------------------------------------------------------- |
30 |
|
|
31 |
|
-module(ejabberd_local). |
32 |
|
-author('alexey@process-one.net'). |
33 |
|
|
34 |
|
-behaviour(gen_server). |
35 |
|
-behaviour(mongoose_packet_handler). |
36 |
|
-behaviour(gen_iq_component). |
37 |
|
|
38 |
|
%% API |
39 |
|
-export([start_link/0]). |
40 |
|
|
41 |
|
-export([process_packet/5, |
42 |
|
route_iq/5, |
43 |
|
route_iq/6, |
44 |
|
process_iq_reply/4, |
45 |
|
register_iq_handler/3, |
46 |
|
register_host/1, |
47 |
|
unregister_iq_handler/2, |
48 |
|
unregister_host/1, |
49 |
|
sync/0 |
50 |
|
]). |
51 |
|
|
52 |
|
%% RPC callbacks |
53 |
|
-export([get_iq_callback/1]). |
54 |
|
|
55 |
|
%% Hooks callbacks |
56 |
|
|
57 |
|
-export([disco_local_features/3]). |
58 |
|
|
59 |
|
%% gen_server callbacks |
60 |
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, |
61 |
|
terminate/2, code_change/3]). |
62 |
|
|
63 |
|
-export([do_route/4]). |
64 |
|
|
65 |
|
-ignore_xref([do_route/4, get_iq_callback/1, |
66 |
|
process_iq_reply/4, start_link/0]). |
67 |
|
|
68 |
|
-include("mongoose.hrl"). |
69 |
|
-include("jlib.hrl"). |
70 |
|
-include("session.hrl"). |
71 |
|
|
72 |
|
-record(state, {}). |
73 |
|
|
74 |
|
-type id() :: binary(). |
75 |
|
-type callback() :: fun((From :: jid:jid(), To :: jid:jid(), |
76 |
|
Acc :: mongoose_acc:t(), IQ :: jlib:iq()) -> |
77 |
|
mongoose_acc:t()) | |
78 |
|
fun((From :: undefined, To :: undefined, |
79 |
|
Acc :: undefined, IQ :: timeout) -> |
80 |
|
undefined). |
81 |
|
|
82 |
|
-define(IQTABLE, local_iqtable). |
83 |
|
-define(NSTABLE, local_nstable). |
84 |
|
-define(IQRESPONSE, local_iqresponse). |
85 |
|
|
86 |
|
%% This value is used in SIP and Megaco for a transaction lifetime. |
87 |
|
-define(IQ_TIMEOUT, 32000). |
88 |
|
|
89 |
|
%%==================================================================== |
90 |
|
%% API |
91 |
|
%%==================================================================== |
92 |
|
%%-------------------------------------------------------------------- |
93 |
|
%% Function: start_link() -> {ok, Pid} | ignore | {error, Error} |
94 |
|
%% Description: Starts the server |
95 |
|
%%-------------------------------------------------------------------- |
96 |
|
-spec start_link() -> 'ignore' | {'error', _} | {'ok', pid()}. |
97 |
|
start_link() -> |
98 |
103 |
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). |
99 |
|
|
100 |
|
-spec process_iq(Acc :: mongoose_acc:t(), |
101 |
|
From :: jid:jid(), |
102 |
|
To :: jid:jid(), |
103 |
|
El :: exml:element() |
104 |
|
) -> mongoose_acc:t(). |
105 |
|
process_iq(Acc0, From, To, El) -> |
106 |
232 |
{IQ, Acc} = mongoose_iq:info(Acc0), |
107 |
232 |
process_iq(IQ, Acc, From, To, El). |
108 |
|
|
109 |
|
process_iq(#iq{ type = Type } = IQReply, Acc, From, To, _El) |
110 |
|
when Type == result; Type == error -> |
111 |
71 |
process_iq_reply(From, To, Acc, IQReply); |
112 |
|
process_iq(#iq{ xmlns = XMLNS } = IQ, Acc, From, To, _El) -> |
113 |
161 |
Host = To#jid.lserver, |
114 |
161 |
case ets:lookup(?IQTABLE, {XMLNS, Host}) of |
115 |
|
[{_, IQHandler}] -> |
116 |
161 |
gen_iq_component:handle(IQHandler, Acc, From, To, IQ); |
117 |
|
[] -> |
118 |
:-( |
T = <<"Local server does not implement this feature">>, |
119 |
:-( |
ejabberd_router:route_error_reply(To, From, Acc, |
120 |
|
mongoose_xmpp_errors:feature_not_implemented(<<"en">>, T)) |
121 |
|
end; |
122 |
|
process_iq(_, Acc, From, To, El) -> |
123 |
:-( |
{Acc1, Err} = jlib:make_error_reply(Acc, El, mongoose_xmpp_errors:bad_request()), |
124 |
:-( |
ejabberd_router:route(To, From, Acc1, Err). |
125 |
|
|
126 |
|
-spec process_iq_reply(From :: jid:jid(), |
127 |
|
To :: jid:jid(), |
128 |
|
Acc :: mongoose_acc:t(), |
129 |
|
IQ :: jlib:iq()) -> mongoose_acc:t(). |
130 |
|
process_iq_reply(From, To, Acc, #iq{id = ID} = IQ) -> |
131 |
71 |
case get_iq_callback_in_cluster(ID, Acc) of |
132 |
|
{ok, Callback} -> |
133 |
71 |
Callback(From, To, Acc, IQ); |
134 |
|
_ -> |
135 |
:-( |
Acc |
136 |
|
end. |
137 |
|
|
138 |
|
-spec get_iq_callback_in_cluster(id(), mongoose_acc:t()) -> |
139 |
|
{ok, callback()} | {error, term()}. |
140 |
|
get_iq_callback_in_cluster(ID, Acc) -> |
141 |
|
%% We store information from which node the request is originating in the ID |
142 |
71 |
case parse_iq_id(ID) of |
143 |
|
local_node -> |
144 |
70 |
get_iq_callback(ID); |
145 |
|
{remote_node, NodeName} -> |
146 |
1 |
rpc:call(NodeName, ?MODULE, get_iq_callback, [ID]); |
147 |
|
{error, Reason} -> |
148 |
:-( |
?LOG_ERROR(#{what => parse_iq_id_failed, |
149 |
:-( |
reason => Reason, acc => Acc}), |
150 |
:-( |
{error, Reason} |
151 |
|
end. |
152 |
|
|
153 |
|
-spec process_packet(Acc :: mongoose_acc:t(), |
154 |
|
From :: jid:jid(), |
155 |
|
To ::jid:jid(), |
156 |
|
El :: exml:element(), |
157 |
|
Extra :: map()) -> mongoose_acc:t(). |
158 |
|
process_packet(Acc, From, To, El, _Extra) -> |
159 |
46329 |
try |
160 |
46329 |
do_route(Acc, From, To, El) |
161 |
|
catch |
162 |
|
Class:Reason:Stacktrace -> |
163 |
:-( |
?LOG_ERROR(#{what => routing_error, acc => Acc, |
164 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}), |
165 |
:-( |
Acc |
166 |
|
end. |
167 |
|
|
168 |
|
-spec route_iq(From :: jid:jid(), |
169 |
|
To :: jid:jid(), |
170 |
|
Acc :: mongoose_acc:t(), |
171 |
|
IQ :: jlib:iq(), |
172 |
|
F :: fun()) -> mongoose_acc:t(). |
173 |
|
route_iq(From, To, Acc, IQ, F) -> |
174 |
74 |
route_iq(From, To, Acc, IQ, F, undefined). |
175 |
|
|
176 |
|
|
177 |
|
%% Send an iq and wait for response |
178 |
|
%% This function is used to route IQs from the server to the client. |
179 |
|
%% A callback function would be called once a response is received from the client. |
180 |
|
-spec route_iq(From :: jid:jid(), |
181 |
|
To :: jid:jid(), |
182 |
|
Acc :: mongoose_acc:t(), |
183 |
|
IQ :: jlib:iq(), |
184 |
|
Callback :: callback(), |
185 |
|
Timeout :: undefined | integer()) -> mongoose_acc:t(). |
186 |
|
route_iq(From, To, Acc, #iq{type = Type} = IQ, Callback, Timeout) |
187 |
|
when is_function(Callback) -> |
188 |
76 |
Packet = case Type == set orelse Type == get of |
189 |
|
true -> |
190 |
76 |
ID = make_iq_id(), |
191 |
76 |
register_iq_response_handler(ID, Callback, Timeout), |
192 |
76 |
jlib:iq_to_xml(IQ#iq{id = ID}); |
193 |
|
false -> |
194 |
:-( |
jlib:iq_to_xml(IQ) |
195 |
|
end, |
196 |
76 |
ejabberd_router:route(From, To, Acc, Packet). |
197 |
|
|
198 |
|
-spec register_iq_response_handler( |
199 |
|
ID :: id(), |
200 |
|
Callback :: callback(), |
201 |
|
Timeout :: undefined | pos_integer()) -> any(). |
202 |
|
register_iq_response_handler(ID, Callback, Timeout0) -> |
203 |
76 |
Timeout = case Timeout0 of |
204 |
|
undefined -> |
205 |
75 |
?IQ_TIMEOUT; |
206 |
|
N when is_integer(N), N > 0 -> |
207 |
1 |
N |
208 |
|
end, |
209 |
76 |
TRef = erlang:start_timer(Timeout, ejabberd_local, ID), |
210 |
76 |
ets:insert(?IQRESPONSE, {ID, Callback, TRef}). |
211 |
|
|
212 |
|
-spec register_iq_handler(Domain :: jid:server(), Namespace :: binary(), |
213 |
|
IQHandler :: mongoose_iq_handler:t()) -> ok. |
214 |
|
register_iq_handler(Domain, XMLNS, IQHandler) -> |
215 |
244 |
ejabberd_local ! {register_iq_handler, Domain, XMLNS, IQHandler}, |
216 |
244 |
ok. |
217 |
|
|
218 |
|
-spec sync() -> ok. |
219 |
|
sync() -> |
220 |
220 |
gen_server:call(ejabberd_local, sync). |
221 |
|
|
222 |
|
-spec unregister_iq_handler(Domain :: jid:server(), Namespace :: binary()) -> ok. |
223 |
|
unregister_iq_handler(Domain, XMLNS) -> |
224 |
195 |
ejabberd_local ! {unregister_iq_handler, Domain, XMLNS}, |
225 |
195 |
ok. |
226 |
|
|
227 |
|
-spec bounce_resource_packet(Acc :: mongoose_acc:t(), |
228 |
|
From :: jid:jid(), |
229 |
|
To :: jid:jid(), |
230 |
|
El :: exml:element()) -> mongoose_acc:t(). |
231 |
|
bounce_resource_packet(Acc, From, To, El) -> |
232 |
2 |
{Acc1, Err} = jlib:make_error_reply(Acc, El, mongoose_xmpp_errors:item_not_found()), |
233 |
2 |
ejabberd_router:route(To, From, Acc1, Err), |
234 |
2 |
Acc. |
235 |
|
|
236 |
|
-spec register_host(Host :: jid:server()) -> ok. |
237 |
|
register_host(Host) -> |
238 |
37 |
gen_server:call(?MODULE, {register_host, Host}). |
239 |
|
|
240 |
|
-spec unregister_host(Host :: jid:server()) -> ok. |
241 |
|
unregister_host(Host) -> |
242 |
13 |
gen_server:call(?MODULE, {unregister_host, Host}). |
243 |
|
|
244 |
|
-spec disco_local_features(mongoose_disco:feature_acc(), |
245 |
|
map(), |
246 |
|
map()) -> {ok, mongoose_disco:feature_acc()}. |
247 |
|
disco_local_features(Acc = #{to_jid := #jid{lserver = LServer}, node := <<>>}, _, _) -> |
248 |
229 |
Features = [Feature || {_, Feature} <- ets:lookup(?NSTABLE, LServer)], |
249 |
229 |
{ok, mongoose_disco:add_features(Features, Acc)}; |
250 |
|
disco_local_features(Acc, _, _) -> |
251 |
5 |
{ok, Acc}. |
252 |
|
|
253 |
|
%%==================================================================== |
254 |
|
%% gen_server callbacks |
255 |
|
%%==================================================================== |
256 |
|
|
257 |
|
%%-------------------------------------------------------------------- |
258 |
|
%% Function: init(Args) -> {ok, State} | |
259 |
|
%% {ok, State, Timeout} | |
260 |
|
%% ignore | |
261 |
|
%% {stop, Reason} |
262 |
|
%% Description: Initiates the server |
263 |
|
%%-------------------------------------------------------------------- |
264 |
|
init([]) -> |
265 |
103 |
catch ets:new(?IQTABLE, [named_table, protected, {read_concurrency, true}]), |
266 |
103 |
catch ets:new(?NSTABLE, [named_table, bag, protected, {read_concurrency, true}]), |
267 |
103 |
catch ets:new(?IQRESPONSE, [named_table, public]), |
268 |
103 |
gen_hook:add_handlers(hooks()), |
269 |
103 |
{ok, #state{}}. |
270 |
|
|
271 |
|
%%-------------------------------------------------------------------- |
272 |
|
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | |
273 |
|
%% {reply, Reply, State, Timeout} | |
274 |
|
%% {noreply, State} | |
275 |
|
%% {noreply, State, Timeout} | |
276 |
|
%% {stop, Reason, Reply, State} | |
277 |
|
%% {stop, Reason, State} |
278 |
|
%% Description: Handling call messages |
279 |
|
%%-------------------------------------------------------------------- |
280 |
|
handle_call({unregister_host, Host}, _From, State) -> |
281 |
13 |
Node = node(), |
282 |
13 |
[mongoose_c2s:stop(Pid, host_was_unregistered) |
283 |
13 |
|| #session{sid = {_, Pid}} <- ejabberd_sm:get_vh_session_list(Host), |
284 |
10 |
node(Pid) =:= Node], |
285 |
13 |
do_unregister_host(Host), |
286 |
13 |
{reply, ok, State}; |
287 |
|
handle_call({register_host, Host}, _From, State) -> |
288 |
37 |
do_register_host(Host), |
289 |
37 |
{reply, ok, State}; |
290 |
|
handle_call(sync, _From, State) -> |
291 |
220 |
{reply, ok, State}; |
292 |
|
handle_call(_Request, _From, State) -> |
293 |
:-( |
Reply = ok, |
294 |
:-( |
{reply, Reply, State}. |
295 |
|
|
296 |
|
%%-------------------------------------------------------------------- |
297 |
|
%% Function: handle_cast(Msg, State) -> {noreply, State} | |
298 |
|
%% {noreply, State, Timeout} | |
299 |
|
%% {stop, Reason, State} |
300 |
|
%% Description: Handling cast messages |
301 |
|
%%-------------------------------------------------------------------- |
302 |
|
handle_cast(_Msg, State) -> |
303 |
:-( |
{noreply, State}. |
304 |
|
|
305 |
|
%%-------------------------------------------------------------------- |
306 |
|
%% Function: handle_info(Info, State) -> {noreply, State} | |
307 |
|
%% {noreply, State, Timeout} | |
308 |
|
%% {stop, Reason, State} |
309 |
|
%% Description: Handling all non call/cast messages |
310 |
|
%%-------------------------------------------------------------------- |
311 |
|
handle_info({route, Acc, From, To, El}, State) -> |
312 |
:-( |
spawn(fun() -> process_packet(Acc, From, To, El, #{}) end), |
313 |
:-( |
{noreply, State}; |
314 |
|
handle_info({register_iq_handler, Host, XMLNS, IQHandler}, State) -> |
315 |
244 |
ets:insert(?NSTABLE, {Host, XMLNS}), |
316 |
244 |
ets:insert(?IQTABLE, {{XMLNS, Host}, IQHandler}), |
317 |
244 |
{noreply, State}; |
318 |
|
handle_info({unregister_iq_handler, Host, XMLNS}, State) -> |
319 |
195 |
case ets:lookup(?IQTABLE, {XMLNS, Host}) of |
320 |
|
[{_, IQHandler}] -> |
321 |
195 |
gen_iq_component:stop_iq_handler(IQHandler), |
322 |
195 |
ets:delete_object(?NSTABLE, {Host, XMLNS}), |
323 |
195 |
ets:delete(?IQTABLE, {XMLNS, Host}); |
324 |
|
_ -> |
325 |
:-( |
ok |
326 |
|
end, |
327 |
195 |
{noreply, State}; |
328 |
|
handle_info({timeout, _TRef, ID}, State) -> |
329 |
5 |
process_iq_timeout(ID), |
330 |
5 |
{noreply, State}; |
331 |
|
handle_info(_Info, State) -> |
332 |
:-( |
{noreply, State}. |
333 |
|
|
334 |
|
%%-------------------------------------------------------------------- |
335 |
|
%% Function: terminate(Reason, State) -> void() |
336 |
|
%% Description: This function is called by a gen_server when it is about to |
337 |
|
%% terminate. It should be the opposite of Module:init/1 and do any necessary |
338 |
|
%% cleaning up. When it returns, the gen_server terminates with Reason. |
339 |
|
%% The return value is ignored. |
340 |
|
%%-------------------------------------------------------------------- |
341 |
|
terminate(_Reason, _State) -> |
342 |
:-( |
gen_hook:delete_handlers(hooks()). |
343 |
|
|
344 |
|
%%-------------------------------------------------------------------- |
345 |
|
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} |
346 |
|
%% Description: Convert process state when code is changed |
347 |
|
%%-------------------------------------------------------------------- |
348 |
|
code_change(_OldVsn, State, _Extra) -> |
349 |
:-( |
{ok, State}. |
350 |
|
|
351 |
|
%%-------------------------------------------------------------------- |
352 |
|
%%% Internal functions |
353 |
|
%%-------------------------------------------------------------------- |
354 |
|
|
355 |
|
hooks() -> |
356 |
103 |
[{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99} |
357 |
103 |
|| HostType <- ?ALL_HOST_TYPES]. |
358 |
|
|
359 |
|
-spec do_route(Acc :: mongoose_acc:t(), |
360 |
|
From :: jid:jid(), |
361 |
|
To :: jid:jid(), |
362 |
|
El :: exml:element()) -> mongoose_acc:t(). |
363 |
|
do_route(Acc, From, To, El) -> |
364 |
46329 |
?LOG_DEBUG(#{what => local_routing, acc => Acc}), |
365 |
46329 |
case directed_to(To) of |
366 |
|
user -> |
367 |
46095 |
ejabberd_sm:route(From, To, Acc, El); |
368 |
|
server -> |
369 |
232 |
case El#xmlel.name of |
370 |
|
<<"iq">> -> |
371 |
232 |
process_iq(Acc, From, To, El); |
372 |
|
_ -> |
373 |
:-( |
Acc |
374 |
|
end; |
375 |
|
local_resource -> |
376 |
2 |
case mongoose_acc:stanza_type(Acc) of |
377 |
:-( |
<<"error">> -> Acc; |
378 |
:-( |
<<"result">> -> Acc; |
379 |
2 |
_ -> bounce_resource_packet(Acc, From, To, El) |
380 |
|
end |
381 |
|
end. |
382 |
|
|
383 |
|
-spec directed_to(jid:jid()) -> user | server | local_resource. |
384 |
|
directed_to(To) -> |
385 |
46329 |
directed_to(To#jid.luser, To#jid.lresource). |
386 |
|
|
387 |
|
directed_to(<<>>, <<>>) -> |
388 |
232 |
server; |
389 |
|
directed_to(<<>>, _) -> |
390 |
2 |
local_resource; |
391 |
|
directed_to(_, _) -> |
392 |
46095 |
user. |
393 |
|
|
394 |
|
-spec get_iq_callback(ID :: id()) -> error | {ok, fun()}. |
395 |
|
get_iq_callback(ID) -> |
396 |
76 |
case ets:lookup(?IQRESPONSE, ID) of |
397 |
|
[{ID, Callback, TRef}] -> |
398 |
76 |
erlang:cancel_timer(TRef), |
399 |
76 |
ets:delete(?IQRESPONSE, ID), |
400 |
76 |
{ok, Callback}; |
401 |
|
_ -> |
402 |
:-( |
error |
403 |
|
end. |
404 |
|
|
405 |
|
-spec process_iq_timeout(id()) -> ok. |
406 |
|
process_iq_timeout(ID) -> |
407 |
5 |
spawn(fun() -> process_iq_timeout2(ID) end), ok. |
408 |
|
|
409 |
|
-spec process_iq_timeout2(id()) -> ok. |
410 |
|
process_iq_timeout2(ID) -> |
411 |
5 |
case get_iq_callback(ID) of |
412 |
|
{ok, Function} -> |
413 |
5 |
Function(undefined, undefined, undefined, timeout), ok; |
414 |
|
_ -> |
415 |
:-( |
ok |
416 |
|
end. |
417 |
|
|
418 |
|
do_register_host(Host) -> |
419 |
37 |
mongoose_router:register_route(Host, mongoose_packet_handler:new(?MODULE)). |
420 |
|
|
421 |
|
do_unregister_host(Host) -> |
422 |
13 |
mongoose_router:unregister_route(Host). |
423 |
|
|
424 |
|
make_iq_id() -> |
425 |
|
%% Attach NodeId, so we know to which node to forward the response |
426 |
76 |
BinNodeId = mongoose_start_node_id:node_id(), |
427 |
76 |
Rand = mongoose_bin:gen_from_crypto(), |
428 |
76 |
<<BinNodeId/binary, "_", Rand/binary>>. |
429 |
|
|
430 |
|
%% Parses ID, made by make_iq_id function |
431 |
|
-spec parse_iq_id(ID :: binary()) -> |
432 |
|
local_node | {remote_node, node()} |
433 |
|
| {error, {unknown_node_id, term()} | bad_iq_format}. |
434 |
|
parse_iq_id(ID) -> |
435 |
71 |
BinNodeId = mongoose_start_node_id:node_id(), |
436 |
71 |
case binary:split(ID, <<"_">>) of |
437 |
|
[BinNodeId, _Rest] -> |
438 |
70 |
local_node; |
439 |
|
[OtherBinNodeId, _Rest] -> |
440 |
1 |
case mongoose_start_node_id:node_id_to_name(OtherBinNodeId) of |
441 |
|
{ok, NodeName} -> |
442 |
1 |
{remote_node, NodeName}; |
443 |
|
{error, Reason} -> |
444 |
:-( |
{error, {unknown_node_id, Reason}} |
445 |
|
end; |
446 |
|
_ -> |
447 |
:-( |
{error, bad_iq_format} |
448 |
|
end. |