1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : ejabberd_s2s.erl |
3 |
|
%%% Author : Alexey Shchepin <alexey@process-one.net> |
4 |
|
%%% Purpose : S2S connections manager |
5 |
|
%%% Created : 7 Dec 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_s2s). |
27 |
|
-author('alexey@process-one.net'). |
28 |
|
|
29 |
|
-behaviour(gen_server). |
30 |
|
-behaviour(xmpp_router). |
31 |
|
|
32 |
|
%% API functions |
33 |
|
-export([start_link/0, |
34 |
|
filter/4, |
35 |
|
route/4, |
36 |
|
key/3, |
37 |
|
try_register/1, |
38 |
|
get_s2s_out_pids/1, |
39 |
|
remove_connection/2]). |
40 |
|
|
41 |
|
%% Hooks callbacks |
42 |
|
-export([node_cleanup/3]). |
43 |
|
|
44 |
|
%% gen_server callbacks |
45 |
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, |
46 |
|
terminate/2, code_change/3]). |
47 |
|
|
48 |
|
-ignore_xref([start_link/0]). |
49 |
|
|
50 |
|
-include("mongoose.hrl"). |
51 |
|
-include("jlib.hrl"). |
52 |
|
|
53 |
|
%% Pair of hosts {FromServer, ToServer}. |
54 |
|
%% FromServer is the local server. |
55 |
|
%% ToServer is the remote server. |
56 |
|
%% Used in a lot of API and backend functions. |
57 |
|
-type fromto() :: {jid:lserver(), jid:lserver()}. |
58 |
|
|
59 |
|
%% Pids for ejabberd_s2s_out servers |
60 |
|
-type s2s_pids() :: [pid()]. |
61 |
|
|
62 |
|
-record(state, {}). |
63 |
|
|
64 |
|
-type base16_secret() :: binary(). |
65 |
|
-type stream_id() :: binary(). |
66 |
|
-type s2s_dialback_key() :: binary(). |
67 |
|
|
68 |
|
-export_type([fromto/0, s2s_pids/0, base16_secret/0, stream_id/0, s2s_dialback_key/0]). |
69 |
|
|
70 |
|
%% API functions |
71 |
|
|
72 |
|
%% Starts the server |
73 |
|
-spec start_link() -> ignore | {error, _} | {ok, pid()}. |
74 |
|
start_link() -> |
75 |
42 |
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). |
76 |
|
|
77 |
|
filter(From, To, Acc, Packet) -> |
78 |
45 |
{From, To, Acc, Packet}. |
79 |
|
|
80 |
|
route(From, To, Acc, Packet) -> |
81 |
45 |
do_route(From, To, Acc, Packet). |
82 |
|
|
83 |
|
%% Called by ejabberd_s2s_out process. |
84 |
|
-spec try_register(fromto()) -> IsRegistered :: boolean(). |
85 |
|
try_register(FromTo) -> |
86 |
:-( |
Pid = self(), |
87 |
:-( |
IsRegistered = call_try_register(Pid, FromTo), |
88 |
:-( |
case IsRegistered of |
89 |
|
false -> |
90 |
|
%% This usually happens when a ejabberd_s2s_out connection is established during dialback |
91 |
|
%% procedure to check the key. |
92 |
|
%% We still are fine, we just would not use that s2s connection to route |
93 |
|
%% any stanzas to the remote server. |
94 |
|
%% Could be a sign of abuse or a bug though, so use logging here. |
95 |
:-( |
?LOG_INFO(#{what => s2s_register_failed, from_to => FromTo, pid => self()}); |
96 |
|
_ -> |
97 |
:-( |
ok |
98 |
|
end, |
99 |
:-( |
IsRegistered. |
100 |
|
|
101 |
|
-spec key(mongooseim:host_type(), fromto(), stream_id()) -> s2s_dialback_key(). |
102 |
|
key(HostType, FromTo, StreamID) -> |
103 |
:-( |
{ok, Secret} = get_shared_secret(HostType), |
104 |
:-( |
mongoose_s2s_dialback:make_key(FromTo, StreamID, Secret). |
105 |
|
|
106 |
|
%% Hooks callbacks |
107 |
|
|
108 |
|
-spec node_cleanup(map(), map(), map()) -> {ok, map()}. |
109 |
|
node_cleanup(Acc, #{node := Node}, _) -> |
110 |
:-( |
Res = call_node_cleanup(Node), |
111 |
:-( |
{ok, maps:put(?MODULE, Res, Acc)}. |
112 |
|
|
113 |
|
%% gen_server callbacks |
114 |
|
|
115 |
|
init([]) -> |
116 |
42 |
internal_database_init(), |
117 |
42 |
set_shared_secret(), |
118 |
42 |
gen_hook:add_handlers(hooks()), |
119 |
42 |
{ok, #state{}}. |
120 |
|
|
121 |
|
handle_call(Request, From, State) -> |
122 |
:-( |
?UNEXPECTED_CALL(Request, From), |
123 |
:-( |
{reply, {error, unexpected_call}, State}. |
124 |
|
|
125 |
|
handle_cast(Msg, State) -> |
126 |
:-( |
?UNEXPECTED_CAST(Msg), |
127 |
:-( |
{noreply, State}. |
128 |
|
|
129 |
|
handle_info(Msg, State) -> |
130 |
:-( |
?UNEXPECTED_INFO(Msg), |
131 |
:-( |
{noreply, State}. |
132 |
|
|
133 |
|
terminate(_Reason, _State) -> |
134 |
:-( |
gen_hook:delete_handlers(hooks()), |
135 |
:-( |
ok. |
136 |
|
|
137 |
|
code_change(_OldVsn, State, _Extra) -> |
138 |
:-( |
{ok, State}. |
139 |
|
|
140 |
|
%%% Internal functions |
141 |
|
-spec hooks() -> [gen_hook:hook_tuple()]. |
142 |
|
hooks() -> |
143 |
42 |
[{node_cleanup, global, fun ?MODULE:node_cleanup/3, #{}, 50}]. |
144 |
|
|
145 |
|
-spec do_route(From :: jid:jid(), |
146 |
|
To :: jid:jid(), |
147 |
|
Acc :: mongoose_acc:t(), |
148 |
|
Packet :: exml:element()) -> |
149 |
|
{done, mongoose_acc:t()}. % this is the 'last resort' router, it always returns 'done'. |
150 |
|
do_route(From, To, Acc, Packet) -> |
151 |
45 |
?LOG_DEBUG(#{what => s2s_route, acc => Acc}), |
152 |
45 |
case find_connection(From, To) of |
153 |
|
{ok, Pid} when is_pid(Pid) -> |
154 |
32 |
?LOG_DEBUG(#{what => s2s_found_connection, |
155 |
|
text => <<"Send packet to s2s connection">>, |
156 |
32 |
s2s_pid => Pid, acc => Acc}), |
157 |
32 |
NewPacket = jlib:replace_from_to(From, To, Packet), |
158 |
32 |
Acc1 = mongoose_hooks:s2s_send_packet(Acc, From, To, Packet), |
159 |
32 |
send_element(Pid, Acc1, NewPacket), |
160 |
32 |
{done, Acc1}; |
161 |
|
{error, _Reason} -> |
162 |
13 |
case mongoose_acc:stanza_type(Acc) of |
163 |
|
<<"error">> -> |
164 |
6 |
{done, Acc}; |
165 |
|
<<"result">> -> |
166 |
:-( |
{done, Acc}; |
167 |
|
_ -> |
168 |
7 |
?LOG_DEBUG(#{what => s2s_connection_not_found, acc => Acc}), |
169 |
7 |
{Acc1, Err} = jlib:make_error_reply( |
170 |
|
Acc, Packet, mongoose_xmpp_errors:service_unavailable()), |
171 |
7 |
Acc2 = ejabberd_router:route(To, From, Acc1, Err), |
172 |
7 |
{done, Acc2} |
173 |
|
end |
174 |
|
end. |
175 |
|
|
176 |
|
-spec send_element(pid(), mongoose_acc:t(), exml:element()) -> ok. |
177 |
|
send_element(Pid, Acc, El) -> |
178 |
32 |
Pid ! {send_element, Acc, El}, |
179 |
32 |
ok. |
180 |
|
|
181 |
|
-spec find_connection(From :: jid:jid(), To :: jid:jid()) -> |
182 |
|
{ok, pid()} | {error, not_allowed}. |
183 |
|
find_connection(From, To) -> |
184 |
45 |
FromTo = mongoose_s2s_lib:make_from_to(From, To), |
185 |
45 |
?LOG_DEBUG(#{what => s2s_find_connection, from_to => FromTo}), |
186 |
45 |
OldCons = get_s2s_out_pids(FromTo), |
187 |
45 |
NewCons = ensure_enough_connections(FromTo, OldCons), |
188 |
45 |
case NewCons of |
189 |
|
[] -> |
190 |
13 |
{error, not_allowed}; |
191 |
|
[_|_] -> |
192 |
32 |
{ok, mongoose_s2s_lib:choose_pid(From, NewCons)} |
193 |
|
end. |
194 |
|
|
195 |
|
%% Opens more connections if needed and allowed. |
196 |
|
%% Returns an updated list of connections. |
197 |
|
-spec ensure_enough_connections(fromto(), s2s_pids()) -> s2s_pids(). |
198 |
|
ensure_enough_connections(FromTo, OldCons) -> |
199 |
45 |
NeededConnections = |
200 |
|
mongoose_s2s_lib:needed_extra_connections_number_if_allowed(FromTo, OldCons), |
201 |
|
%% Could be negative, if we have too many connections |
202 |
45 |
case NeededConnections > 0 of |
203 |
|
true -> |
204 |
9 |
open_new_connections(NeededConnections, FromTo), |
205 |
|
%% Query for s2s pids one more time |
206 |
9 |
get_s2s_out_pids(FromTo); |
207 |
|
false -> |
208 |
36 |
OldCons |
209 |
|
end. |
210 |
|
|
211 |
|
-spec open_new_connections(N :: pos_integer(), FromTo :: fromto()) -> ok. |
212 |
|
open_new_connections(N, FromTo) -> |
213 |
9 |
[open_new_connection(FromTo) || _N <- lists:seq(1, N)], |
214 |
9 |
ok. |
215 |
|
|
216 |
|
-spec open_new_connection(FromTo :: fromto()) -> ok. |
217 |
|
open_new_connection(FromTo) -> |
218 |
|
%% Start a process, but do not connect to the server yet. |
219 |
9 |
{ok, Pid} = ejabberd_s2s_out:start(FromTo, new), |
220 |
|
%% Try to write the Pid into Mnesia/CETS |
221 |
9 |
IsRegistered = call_try_register(Pid, FromTo), |
222 |
9 |
maybe_start_connection(Pid, FromTo, IsRegistered), |
223 |
9 |
ok. |
224 |
|
|
225 |
|
%% If registration is successful, create an actual network connection. |
226 |
|
%% If not successful, remove the process. |
227 |
|
-spec maybe_start_connection(Pid :: pid(), FromTo :: fromto(), IsRegistered :: boolean()) -> ok. |
228 |
|
maybe_start_connection(Pid, FromTo, true) -> |
229 |
9 |
?LOG_INFO(#{what => s2s_new_connection, |
230 |
|
text => <<"New s2s connection started">>, |
231 |
9 |
from_to => FromTo, s2s_pid => Pid}), |
232 |
9 |
ejabberd_s2s_out:start_connection(Pid); |
233 |
|
maybe_start_connection(Pid, _FromTo, false) -> |
234 |
:-( |
ejabberd_s2s_out:stop_connection(Pid). |
235 |
|
|
236 |
|
-spec set_shared_secret() -> ok. |
237 |
|
set_shared_secret() -> |
238 |
42 |
[set_shared_secret(HostType) || HostType <- ?ALL_HOST_TYPES], |
239 |
42 |
ok. |
240 |
|
|
241 |
|
%% Updates the secret across the cluster if needed |
242 |
|
-spec set_shared_secret(mongooseim:host_type()) -> ok. |
243 |
|
set_shared_secret(HostType) -> |
244 |
233 |
case mongoose_s2s_lib:check_shared_secret(HostType, get_shared_secret(HostType)) of |
245 |
|
{update, NewSecret} -> |
246 |
188 |
register_secret(HostType, NewSecret); |
247 |
|
ok -> |
248 |
45 |
ok |
249 |
|
end. |
250 |
|
|
251 |
|
%% Backend logic functions |
252 |
|
|
253 |
|
-spec internal_database_init() -> ok. |
254 |
|
internal_database_init() -> |
255 |
42 |
Backend = mongoose_config:get_opt(s2s_backend), |
256 |
42 |
mongoose_s2s_backend:init(#{backend => Backend}). |
257 |
|
|
258 |
|
%% Get ejabberd_s2s_out pids |
259 |
|
-spec get_s2s_out_pids(FromTo :: fromto()) -> s2s_pids(). |
260 |
|
get_s2s_out_pids(FromTo) -> |
261 |
54 |
mongoose_s2s_backend:get_s2s_out_pids(FromTo). |
262 |
|
|
263 |
|
%% Returns true if the connection is registered |
264 |
|
-spec call_try_register(Pid :: pid(), FromTo :: fromto()) -> IsRegistered :: boolean(). |
265 |
|
call_try_register(Pid, FromTo) -> |
266 |
9 |
mongoose_s2s_backend:try_register(Pid, FromTo). |
267 |
|
|
268 |
|
-spec call_node_cleanup(Node :: node()) -> ok. |
269 |
|
call_node_cleanup(Node) -> |
270 |
:-( |
mongoose_s2s_backend:node_cleanup(Node). |
271 |
|
|
272 |
|
-spec remove_connection(fromto(), pid()) -> ok. |
273 |
|
remove_connection(FromTo, Pid) -> |
274 |
9 |
mongoose_s2s_backend:remove_connection(FromTo, Pid). |
275 |
|
|
276 |
|
-spec get_shared_secret(mongooseim:host_type()) -> {ok, base16_secret()} | {error, not_found}. |
277 |
|
get_shared_secret(HostType) -> |
278 |
233 |
mongoose_s2s_backend:get_shared_secret(HostType). |
279 |
|
|
280 |
|
-spec register_secret(mongooseim:host_type(), base16_secret()) -> ok. |
281 |
|
register_secret(HostType, Secret) -> |
282 |
188 |
mongoose_s2s_backend:register_secret(HostType, Secret). |