./ct_report/coverage/mongoose_s2s_lib.COVER.html

1 %% Library functions without side effects.
2 %% These functions do not change the state of the system or send any messages.
3 %% These functions do not write into Mnesia/CETS or read from it.
4 %% They could read the configuration table though.
5 %% There is one hook `mongoose_hooks:s2s_allow_host', that could cause some side effects
6 %% (it depends on the hook handlers).
7 -module(mongoose_s2s_lib).
8 -export([make_from_to/2,
9 timeout/0,
10 domain_utf8_to_ascii/1,
11 check_shared_secret/2,
12 lookup_certfile/1,
13 choose_pid/2,
14 need_more_connections/2,
15 needed_extra_connections_number_if_allowed/2,
16 allow_host/1]).
17
18 -include("mongoose.hrl").
19 -include("jlib.hrl").
20
21 -type fromto() :: ejabberd_s2s:fromto().
22 -type s2s_pids() :: ejabberd_s2s:s2s_pids().
23
24 -define(DEFAULT_MAX_S2S_CONNECTIONS, 1).
25 -define(DEFAULT_MAX_S2S_CONNECTIONS_PER_NODE, 1).
26
27 -spec make_from_to(From :: jid:jid(), To :: jid:jid()) -> fromto().
28 make_from_to(#jid{lserver = FromServer}, #jid{lserver = ToServer}) ->
29 131 {FromServer, ToServer}.
30
31 timeout() ->
32 309 600000.
33
34 %% Converts a UTF-8 domain to ASCII (IDNA)
35 -spec domain_utf8_to_ascii(jid:server()) -> jid:server() | false.
36 domain_utf8_to_ascii(Domain) ->
37 13 case catch idna:utf8_to_ascii(Domain) of
38 {'EXIT', _} ->
39 2 false;
40 AsciiDomain ->
41 11 list_to_binary(AsciiDomain)
42 end.
43
44 -spec check_shared_secret(HostType, StoredSecretResult) -> ok | {update, NewSecret} when
45 HostType :: mongooseim:host_type(),
46 StoredSecretResult :: {ok, ejabberd_s2s:base16_secret()} | {error, not_found},
47 NewSecret :: ejabberd_s2s:base16_secret().
48 check_shared_secret(HostType, StoredSecretResult) ->
49 %% register_secret is replicated across all nodes.
50 %% So, when starting a node with updated secret in the config,
51 %% we would replace stored secret on all nodes at once.
52 %% There could be a small race condition when dialback key checks would get rejected,
53 %% But there would not be conflicts when some nodes have one secret stored and others - another.
54 569 case {StoredSecretResult, get_shared_secret_from_config(HostType)} of
55 {{error, not_found}, {ok, Secret}} ->
56 %% Write the secret from the config into Mnesia/CETS for the first time
57
:-(
{update, Secret};
58 {{error, not_found}, {error, not_found}} ->
59 %% Write a random secret into Mnesia/CETS for the first time
60 257 {update, make_random_secret()};
61 {{ok, Secret}, {ok, Secret}} ->
62 %% Config matches Mnesia/CETS
63
:-(
ok;
64 {{ok, _OldSecret}, {ok, NewSecret}} ->
65 2 ?LOG_INFO(#{what => overwrite_secret_from_config}),
66 2 {update, NewSecret};
67 {{ok, _OldSecret}, {error, not_found}} ->
68 %% Keep the secret already stored in Mnesia/CETS
69 310 ok
70 end.
71
72 -spec make_random_secret() -> ejabberd_s2s:base16_secret().
73 make_random_secret() ->
74 257 base16:encode(crypto:strong_rand_bytes(10)).
75
76 -spec get_shared_secret_from_config(mongooseim:host_type()) ->
77 {ok, ejabberd_s2s:base16_secret()} | {error, not_found}.
78 get_shared_secret_from_config(HostType) ->
79 569 mongoose_config:lookup_opt([{s2s, HostType}, shared]).
80
81 -spec lookup_certfile(mongooseim:host_type()) -> {ok, string()} | {error, not_found}.
82 lookup_certfile(HostType) ->
83 82 case mongoose_config:lookup_opt({domain_certfile, HostType}) of
84 {ok, CertFile} ->
85
:-(
CertFile;
86 {error, not_found} ->
87 82 mongoose_config:lookup_opt([{s2s, HostType}, certfile])
88 end.
89
90 %% Prefers the local connection (i.e. not on the remote node)
91 -spec choose_pid(From :: jid:jid(), Pids :: s2s_pids()) -> pid().
92 choose_pid(From, [_|_] = Pids) ->
93 116 Pids1 = case filter_local_pids(Pids) of
94
:-(
[] -> Pids;
95 116 FilteredPids -> FilteredPids
96 end,
97 % Use sticky connections based on the JID of the sender
98 % (without the resource to ensure that a muc room always uses the same connection)
99 116 Pid = lists:nth(erlang:phash2(jid:to_bare(From), length(Pids1)) + 1, Pids1),
100 116 ?LOG_DEBUG(#{what => s2s_choose_pid, from => From, s2s_pid => Pid}),
101 116 Pid.
102
103 %% Returns only pids from the current node.
104 -spec filter_local_pids(s2s_pids()) -> s2s_pids().
105 filter_local_pids(Pids) ->
106 296 Node = node(),
107 296 [Pid || Pid <- Pids, node(Pid) == Node].
108
109 -spec max_s2s_connections(fromto()) -> pos_integer().
110 max_s2s_connections(FromTo) ->
111 180 match_integer_acl_rule(FromTo, max_s2s_connections,
112 ?DEFAULT_MAX_S2S_CONNECTIONS).
113
114 -spec max_s2s_connections_per_node(fromto()) -> pos_integer().
115 max_s2s_connections_per_node(FromTo) ->
116 180 match_integer_acl_rule(FromTo, max_s2s_connections_per_node,
117 ?DEFAULT_MAX_S2S_CONNECTIONS_PER_NODE).
118
119 -spec match_integer_acl_rule(fromto(), atom(), integer()) -> term().
120 match_integer_acl_rule({FromServer, ToServer}, Rule, Default) ->
121 360 {ok, HostType} = mongoose_domain_api:get_host_type(FromServer),
122 360 ToServerJid = jid:make(<<>>, ToServer, <<>>),
123 360 case acl:match_rule(HostType, Rule, ToServerJid) of
124
:-(
Int when is_integer(Int) -> Int;
125 360 _ -> Default
126 end.
127
128 -spec needed_extra_connections_number_if_allowed(fromto(), s2s_pids()) -> non_neg_integer().
129 needed_extra_connections_number_if_allowed(FromTo, OldCons) ->
130 131 case is_s2s_allowed_for_host(FromTo, OldCons) of
131 true ->
132 116 needed_extra_connections_number(FromTo, OldCons);
133 false ->
134 15 0
135 end.
136
137 %% Checks:
138 %% - if the host is not a service
139 %% - and host policy (allowlist or denylist)
140 -spec is_s2s_allowed_for_host(fromto(), _OldConnections :: s2s_pids()) -> boolean().
141 is_s2s_allowed_for_host(_FromTo, [_|_]) ->
142 84 true; %% Has outgoing connections established, skip the check
143 is_s2s_allowed_for_host(FromTo, []) ->
144 47 not is_service(FromTo) andalso allow_host(FromTo).
145
146 %% Checks if the s2s host is not in the denylist or is in the allowlist
147 %% Runs a hook
148 -spec allow_host(fromto()) -> boolean().
149 allow_host({FromServer, ToServer}) ->
150 71 case mongoose_domain_api:get_host_type(FromServer) of
151 {error, not_found} ->
152 12 false;
153 {ok, HostType} ->
154 59 case mongoose_config:lookup_opt([{s2s, HostType}, host_policy, ToServer]) of
155 {ok, allow} ->
156
:-(
true;
157 {ok, deny} ->
158
:-(
false;
159 {error, not_found} ->
160 59 mongoose_config:get_opt([{s2s, HostType}, default_policy]) =:= allow
161 59 andalso mongoose_hooks:s2s_allow_host(FromServer, ToServer) =:= allow
162 end
163 end.
164
165 -spec need_more_connections(fromto(), s2s_pids()) -> boolean().
166 need_more_connections(FromTo, Connections) ->
167 64 needed_extra_connections_number(FromTo, Connections) > 0.
168
169 -spec needed_extra_connections_number(fromto(), s2s_pids()) -> non_neg_integer().
170 needed_extra_connections_number(FromTo, Connections) ->
171 180 MaxConnections = max_s2s_connections(FromTo),
172 180 MaxConnectionsPerNode = max_s2s_connections_per_node(FromTo),
173 180 LocalPids = filter_local_pids(Connections),
174 180 lists:min([MaxConnections - length(Connections),
175 MaxConnectionsPerNode - length(LocalPids)]).
176
177 %% Returns true if the destination must be considered as a service.
178 -spec is_service(ejabberd_s2s:fromto()) -> boolean().
179 is_service({FromServer, ToServer} = _FromTo) ->
180 47 case mongoose_config:lookup_opt({route_subdomains, FromServer}) of
181 {ok, s2s} -> % bypass RFC 3920 10.3
182
:-(
false;
183 {error, not_found} ->
184 47 Hosts = ?MYHOSTS,
185 47 P = fun(ParentDomain) -> lists:member(ParentDomain, Hosts) end,
186 47 lists:any(P, parent_domains(ToServer))
187 end.
188
189 -spec parent_domains(jid:lserver()) -> [jid:lserver()].
190 parent_domains(Domain) ->
191 47 parent_domains(Domain, [Domain]).
192
193 parent_domains(<<>>, Acc) ->
194 47 lists:reverse(Acc);
195 parent_domains(<<$., Rest/binary>>, Acc) ->
196 33 parent_domains(Rest, [Rest | Acc]);
197 parent_domains(<<_, Rest/binary>>, Acc) ->
198 482 parent_domains(Rest, Acc).
Line Hits Source