1 |
|
%%%=================================================================== |
2 |
|
%%% @copyright (C) 2013, Erlang Solutions Ltd. |
3 |
|
%%% @doc Cowboy based BOSH support for MongooseIM |
4 |
|
%%% |
5 |
|
%%% @end |
6 |
|
%%%=================================================================== |
7 |
|
-module(mod_bosh). |
8 |
|
-behaviour(gen_mod). |
9 |
|
-behaviour(mongoose_module_metrics). |
10 |
|
%% cowboy_loop is a long polling handler |
11 |
|
-behaviour(cowboy_loop). |
12 |
|
|
13 |
|
-xep([{xep, 206}, {version, "1.4"}]). |
14 |
|
-xep([{xep, 124}, {version, "1.11.2"}]). |
15 |
|
|
16 |
|
%% gen_mod callbacks |
17 |
|
-export([start/2, |
18 |
|
stop/1, |
19 |
|
config_spec/0, |
20 |
|
supported_features/0]). |
21 |
|
|
22 |
|
%% cowboy_loop_handler callbacks |
23 |
|
-export([init/2, |
24 |
|
info/3, |
25 |
|
terminate/3]). |
26 |
|
|
27 |
|
%% Hooks callbacks |
28 |
|
-export([node_cleanup/3]). |
29 |
|
|
30 |
|
%% For testing and debugging |
31 |
|
-export([get_session_socket/1, store_session/2]). |
32 |
|
|
33 |
|
-export([config_metrics/1]). |
34 |
|
|
35 |
|
-ignore_xref([get_session_socket/1, store_session/2]). |
36 |
|
|
37 |
|
-include("mongoose.hrl"). |
38 |
|
-include("jlib.hrl"). |
39 |
|
-include_lib("exml/include/exml_stream.hrl"). |
40 |
|
-include("mod_bosh.hrl"). |
41 |
|
-include("mongoose_config_spec.hrl"). |
42 |
|
|
43 |
|
-define(DEFAULT_MAX_AGE, 1728000). %% 20 days in seconds |
44 |
|
-define(DEFAULT_ALLOW_ORIGIN, <<"*">>). |
45 |
|
|
46 |
|
-export_type([session/0, |
47 |
|
sid/0, |
48 |
|
event_type/0, |
49 |
|
socket/0 |
50 |
|
]). |
51 |
|
|
52 |
|
-type socket() :: #bosh_socket{}. |
53 |
|
-type session() :: #bosh_session{ |
54 |
|
sid :: mod_bosh:sid(), |
55 |
|
socket :: pid() |
56 |
|
}. |
57 |
|
-type sid() :: binary(). |
58 |
|
-type event_type() :: streamstart |
59 |
|
| restart |
60 |
|
| normal |
61 |
|
| pause |
62 |
|
| streamend. |
63 |
|
|
64 |
|
-type headers_list() :: [{binary(), binary()}]. |
65 |
|
|
66 |
|
%% Request State |
67 |
|
-record(rstate, {req_sid, opts}). |
68 |
|
-type rstate() :: #rstate{}. |
69 |
|
-type req() :: cowboy_req:req(). |
70 |
|
|
71 |
|
-type info() :: accept_options |
72 |
|
| accept_get |
73 |
|
| item_not_found |
74 |
|
| no_body |
75 |
|
| policy_violation |
76 |
|
| {bosh_reply, exml:element()} |
77 |
|
| {close, _} |
78 |
|
| {wrong_method, _}. |
79 |
|
|
80 |
|
%%-------------------------------------------------------------------- |
81 |
|
%% gen_mod callbacks |
82 |
|
%%-------------------------------------------------------------------- |
83 |
|
|
84 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
85 |
|
start(_HostType, Opts) -> |
86 |
377 |
case mod_bosh_socket:is_supervisor_started() of |
87 |
|
true -> |
88 |
284 |
ok; % There is only one backend implementation (mnesia), so it is started globally |
89 |
|
false -> |
90 |
93 |
mod_bosh_backend:start(Opts), |
91 |
93 |
{ok, _Pid} = mod_bosh_socket:start_supervisor(), |
92 |
93 |
gen_hook:add_handlers(hooks()) |
93 |
|
end. |
94 |
|
|
95 |
|
-spec stop(mongooseim:host_type()) -> ok. |
96 |
|
stop(_HostType) -> |
97 |
377 |
gen_hook:delete_handlers(hooks()), |
98 |
377 |
ok. |
99 |
|
|
100 |
|
-spec hooks() -> gen_hook:hook_list(). |
101 |
|
hooks() -> |
102 |
470 |
[{node_cleanup, global, fun ?MODULE:node_cleanup/3, #{}, 50}]. |
103 |
|
|
104 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
105 |
|
config_spec() -> |
106 |
186 |
#section{items = #{<<"backend">> => #option{type = atom, |
107 |
|
validate = {module, mod_bosh}}, |
108 |
|
<<"inactivity">> => #option{type = int_or_infinity, |
109 |
|
validate = positive}, |
110 |
|
<<"max_wait">> => #option{type = int_or_infinity, |
111 |
|
validate = positive}, |
112 |
|
<<"server_acks">> => #option{type = boolean}, |
113 |
|
<<"max_pause">> => #option{type = integer, |
114 |
|
validate = positive} |
115 |
|
}, |
116 |
|
defaults = #{<<"backend">> => mnesia, |
117 |
|
<<"inactivity">> => 30, % seconds |
118 |
|
<<"max_wait">> => infinity, % seconds |
119 |
|
<<"server_acks">> => false, |
120 |
|
<<"max_pause">> => 120} % seconds |
121 |
|
}. |
122 |
|
|
123 |
|
-spec supported_features() -> [atom()]. |
124 |
|
supported_features() -> |
125 |
193 |
[dynamic_domains]. |
126 |
|
|
127 |
|
%%-------------------------------------------------------------------- |
128 |
|
%% Hooks handlers |
129 |
|
%%-------------------------------------------------------------------- |
130 |
|
|
131 |
|
-spec node_cleanup(Acc, Params, Extra) -> {ok, Acc} when |
132 |
|
Acc :: map(), |
133 |
|
Params :: #{node := node()}, |
134 |
|
Extra :: gen_hook:extra(). |
135 |
|
node_cleanup(Acc, #{node := Node}, _) -> |
136 |
8 |
Res = mod_bosh_backend:node_cleanup(Node), |
137 |
8 |
{ok, maps:put(?MODULE, Res, Acc)}. |
138 |
|
|
139 |
|
%%-------------------------------------------------------------------- |
140 |
|
%% cowboy_loop_handler callbacks |
141 |
|
%%-------------------------------------------------------------------- |
142 |
|
|
143 |
|
-spec init(req(), mongoose_http_handler:options()) -> {cowboy_loop, req(), rstate()}. |
144 |
|
init(Req, Opts) -> |
145 |
2025 |
?LOG_DEBUG(#{what => bosh_init, req => Req}), |
146 |
2025 |
Msg = init_msg(Req), |
147 |
2025 |
self() ! Msg, |
148 |
|
%% Upgrade to cowboy_loop behaviour to enable long polling |
149 |
2025 |
{cowboy_loop, Req, #rstate{opts = Opts}}. |
150 |
|
|
151 |
|
|
152 |
|
%% ok return keep the handler looping. |
153 |
|
%% stop handler is used to reply to the client. |
154 |
|
-spec info(info(), req(), rstate()) -> {ok, req(), _} | {stop, req(), _}. |
155 |
|
info(accept_options, Req, State) -> |
156 |
2 |
Origin = cowboy_req:header(<<"origin">>, Req), |
157 |
2 |
Headers = ac_all(Origin), |
158 |
2 |
?LOG_DEBUG(#{what => bosh_accept_options, headers => Headers, |
159 |
2 |
text => <<"Handle OPTIONS request in Bosh">>}), |
160 |
2 |
Req1 = cowboy_reply(200, Headers, <<>>, Req), |
161 |
2 |
{stop, Req1, State}; |
162 |
|
info(accept_get, Req, State) -> |
163 |
2 |
Headers = [content_type(), |
164 |
|
ac_allow_methods(), |
165 |
|
ac_allow_headers(), |
166 |
|
ac_max_age()], |
167 |
2 |
Body = <<"MongooseIM bosh endpoint">>, |
168 |
2 |
Req1 = cowboy_reply(200, Headers, Body, Req), |
169 |
2 |
{stop, Req1, State}; |
170 |
|
info(forward_body, Req, S) -> |
171 |
2017 |
{ok, Body, Req1} = cowboy_req:read_body(Req), |
172 |
|
%% TODO: the parser should be stored per session, |
173 |
|
%% but the session is identified inside the to-be-parsed element |
174 |
2017 |
{ok, BodyElem} = exml:parse(Body), |
175 |
2017 |
Sid = exml_query:attr(BodyElem, <<"sid">>, <<"missing">>), |
176 |
2017 |
?LOG_DEBUG(#{what => bosh_receive, sid => Sid, request_body => Body}), |
177 |
|
%% Remember req_sid, so it can be used to print a debug message in bosh_reply |
178 |
2017 |
forward_body(Req1, BodyElem, S#rstate{req_sid = Sid}); |
179 |
|
info({bosh_reply, El}, Req, S) -> |
180 |
2007 |
BEl = exml:to_binary(El), |
181 |
|
%% for BOSH 'data.xmpp.sent.raw' metric includes 'body' wrapping elements |
182 |
|
%% and resending attempts |
183 |
2007 |
mongoose_metrics:update(global, [data, xmpp, sent, c2s, bosh], byte_size(BEl)), |
184 |
2007 |
?LOG_DEBUG(#{what => bosh_send, req_sid => S#rstate.req_sid, reply_body => BEl, |
185 |
2007 |
sid => exml_query:attr(El, <<"sid">>, <<"missing">>)}), |
186 |
2007 |
Headers = bosh_reply_headers(), |
187 |
2007 |
Req1 = cowboy_reply(200, Headers, BEl, Req), |
188 |
2007 |
{stop, Req1, S}; |
189 |
|
|
190 |
|
info({close, Sid}, Req, S) -> |
191 |
:-( |
?LOG_DEBUG(#{what => bosh_close, sid => Sid}), |
192 |
:-( |
Req1 = cowboy_reply(200, [], <<>>, Req), |
193 |
:-( |
{stop, Req1, S}; |
194 |
|
info(no_body, Req, State) -> |
195 |
2 |
?LOG_DEBUG(#{what => bosh_stop, reason => missing_request_body, req => Req}), |
196 |
2 |
Req1 = no_body_error(Req), |
197 |
2 |
{stop, Req1, State}; |
198 |
|
info({wrong_method, Method}, Req, State) -> |
199 |
2 |
?LOG_DEBUG(#{what => bosh_stop, reason => wrong_request_method, |
200 |
2 |
method => Method, req => Req}), |
201 |
2 |
Req1 = method_not_allowed_error(Req), |
202 |
2 |
{stop, Req1, State}; |
203 |
|
info(item_not_found, Req, S) -> |
204 |
4 |
Req1 = terminal_condition(<<"item-not-found">>, Req), |
205 |
4 |
{stop, Req1, S}; |
206 |
|
info(policy_violation, Req, S) -> |
207 |
2 |
Req1 = terminal_condition(<<"policy-violation">>, Req), |
208 |
2 |
{stop, Req1, S}. |
209 |
|
|
210 |
|
|
211 |
|
terminate(_Reason, _Req, _State) -> |
212 |
2024 |
?LOG_DEBUG(#{what => bosh_terminate}), |
213 |
2024 |
ok. |
214 |
|
|
215 |
|
%%-------------------------------------------------------------------- |
216 |
|
%% Callbacks implementation |
217 |
|
%%-------------------------------------------------------------------- |
218 |
|
|
219 |
|
init_msg(Req) -> |
220 |
2025 |
Method = cowboy_req:method(Req), |
221 |
2025 |
case Method of |
222 |
|
<<"OPTIONS">> -> |
223 |
2 |
accept_options; |
224 |
|
<<"POST">> -> |
225 |
2019 |
case cowboy_req:has_body(Req) of |
226 |
|
true -> |
227 |
2017 |
forward_body; |
228 |
|
false -> |
229 |
2 |
no_body |
230 |
|
end; |
231 |
|
<<"GET">> -> |
232 |
2 |
accept_get; |
233 |
|
_ -> |
234 |
2 |
{wrong_method, Method} |
235 |
|
end. |
236 |
|
|
237 |
|
-spec to_event_type(exml:element()) -> event_type(). |
238 |
|
to_event_type(Body) -> |
239 |
|
%% Order of checks is important: |
240 |
|
%% stream restart has got sid attribute, |
241 |
|
%% so check for it at the end. |
242 |
2017 |
check_event_type_streamend(Body). |
243 |
|
|
244 |
|
check_event_type_streamend(Body) -> |
245 |
2017 |
case exml_query:attr(Body, <<"type">>) of |
246 |
|
<<"terminate">> -> |
247 |
117 |
streamend; |
248 |
|
_ -> |
249 |
1900 |
check_event_type_restart(Body) |
250 |
|
end. |
251 |
|
|
252 |
|
check_event_type_restart(Body) -> |
253 |
1900 |
case exml_query:attr(Body, <<"xmpp:restart">>) of |
254 |
|
<<"true">> -> |
255 |
120 |
restart; |
256 |
|
_ -> |
257 |
1780 |
check_event_type_pause(Body) |
258 |
|
end. |
259 |
|
|
260 |
|
check_event_type_pause(Body) -> |
261 |
1780 |
case exml_query:attr(Body, <<"pause">>) of |
262 |
|
undefined -> |
263 |
1777 |
check_event_type_streamstrart(Body); |
264 |
|
_ -> |
265 |
3 |
pause |
266 |
|
end. |
267 |
|
|
268 |
|
check_event_type_streamstrart(Body) -> |
269 |
1777 |
case exml_query:attr(Body, <<"sid">>) of |
270 |
|
undefined -> |
271 |
127 |
streamstart; |
272 |
|
_ -> |
273 |
1650 |
normal |
274 |
|
end. |
275 |
|
|
276 |
|
-spec forward_body(req(), exml:element(), rstate()) |
277 |
|
-> {ok, req(), rstate()} | {stop, req(), rstate()}. |
278 |
|
forward_body(Req, #xmlel{} = Body, #rstate{opts = Opts} = S) -> |
279 |
2017 |
Type = to_event_type(Body), |
280 |
2017 |
case Type of |
281 |
|
streamstart -> |
282 |
127 |
{SessionStarted, Req1} = maybe_start_session(Req, Body, Opts), |
283 |
127 |
case SessionStarted of |
284 |
|
true -> |
285 |
124 |
{ok, Req1, S}; |
286 |
|
false -> |
287 |
3 |
{stop, Req1, S} |
288 |
|
end; |
289 |
|
_ -> |
290 |
1890 |
Sid = exml_query:attr(Body, <<"sid">>), |
291 |
1890 |
case get_session_socket(Sid) of |
292 |
|
{ok, Socket} -> |
293 |
|
%% Forward request from a client to c2s process |
294 |
1890 |
handle_request(Socket, Type, Body), |
295 |
1890 |
{ok, Req, S}; |
296 |
|
{error, item_not_found} -> |
297 |
:-( |
?LOG_WARNING(#{what => bosh_stop, reason => session_not_found, |
298 |
:-( |
sid => Sid}), |
299 |
:-( |
Req1 = terminal_condition(<<"item-not-found">>, Req), |
300 |
:-( |
{stop, Req1, S} |
301 |
|
end |
302 |
|
end. |
303 |
|
|
304 |
|
|
305 |
|
-spec handle_request(pid(), event_type(), exml:element()) -> ok. |
306 |
|
handle_request(Socket, EventType, Body) -> |
307 |
|
%% for BOSH 'data.xmpp.received.raw' metric includes 'body' wrapping elements |
308 |
2014 |
mongoose_metrics:update(global, [data, xmpp, received, c2s, bosh], exml:xml_size(Body)), |
309 |
2014 |
mod_bosh_socket:handle_request(Socket, {EventType, self(), Body}). |
310 |
|
|
311 |
|
|
312 |
|
-spec get_session_socket(mod_bosh:sid()) -> {ok, pid()} | {error, item_not_found}. |
313 |
|
get_session_socket(Sid) -> |
314 |
1890 |
case mod_bosh_backend:get_session(Sid) of |
315 |
|
[BS] -> |
316 |
1890 |
{ok, BS#bosh_session.socket}; |
317 |
|
[] -> |
318 |
:-( |
{error, item_not_found} |
319 |
|
end. |
320 |
|
|
321 |
|
|
322 |
|
-spec maybe_start_session(req(), exml:element(), map()) -> |
323 |
|
{SessionStarted :: boolean(), req()}. |
324 |
|
maybe_start_session(Req, Body, Opts) -> |
325 |
127 |
Domain = exml_query:attr(Body, <<"to">>), |
326 |
127 |
case mongoose_domain_api:get_domain_host_type(Domain) of |
327 |
|
{ok, HostType} -> |
328 |
127 |
case gen_mod:is_loaded(HostType, ?MODULE) of |
329 |
|
true -> |
330 |
126 |
maybe_start_session_on_known_host(HostType, Req, Body, Opts); |
331 |
|
false -> |
332 |
1 |
{false, terminal_condition(<<"host-unknown">>, Req)} |
333 |
|
end; |
334 |
|
{error, not_found} -> |
335 |
:-( |
{false, terminal_condition(<<"host-unknown">>, Req)} |
336 |
|
end. |
337 |
|
|
338 |
|
-spec maybe_start_session_on_known_host(mongooseim:host_type(), req(), exml:element(), map()) -> |
339 |
|
{SessionStarted :: boolean(), req()}. |
340 |
|
maybe_start_session_on_known_host(HostType, Req, Body, Opts) -> |
341 |
126 |
try |
342 |
126 |
maybe_start_session_on_known_host_unsafe(HostType, Req, Body, Opts) |
343 |
|
catch |
344 |
|
error:Reason:Stacktrace -> |
345 |
|
%% It's here because something catch-y was here before |
346 |
2 |
?LOG_ERROR(#{what => bosh_stop, issue => undefined_condition, |
347 |
:-( |
reason => Reason, stacktrace => Stacktrace}), |
348 |
2 |
Req1 = terminal_condition(<<"undefined-condition">>, [], Req), |
349 |
2 |
{false, Req1} |
350 |
|
end. |
351 |
|
|
352 |
|
-spec maybe_start_session_on_known_host_unsafe(mongooseim:host_type(), req(), exml:element(), map()) -> |
353 |
|
{SessionStarted :: boolean(), req()}. |
354 |
|
maybe_start_session_on_known_host_unsafe(HostType, Req, Body, Opts) -> |
355 |
|
%% Version isn't checked as it would be meaningless when supporting |
356 |
|
%% only a subset of the specification. |
357 |
126 |
{ok, NewBody} = set_max_hold(Body), |
358 |
124 |
Peer = cowboy_req:peer(Req), |
359 |
124 |
PeerCert = cowboy_req:cert(Req), |
360 |
124 |
start_session(HostType, Peer, PeerCert, NewBody, Opts), |
361 |
124 |
{true, Req}. |
362 |
|
|
363 |
|
-spec start_session(mongooseim:host_type(), mongoose_transport:peer(), |
364 |
|
binary() | undefined, exml:element(), map()) -> any(). |
365 |
|
start_session(HostType, Peer, PeerCert, Body, Opts) -> |
366 |
124 |
Sid = make_sid(), |
367 |
124 |
{ok, Socket} = mod_bosh_socket:start(HostType, Sid, Peer, PeerCert, Opts), |
368 |
124 |
store_session(Sid, Socket), |
369 |
124 |
handle_request(Socket, streamstart, Body), |
370 |
124 |
?LOG_DEBUG(#{what => bosh_start_session, sid => Sid}). |
371 |
|
|
372 |
|
-spec store_session(Sid :: sid(), Socket :: pid()) -> any(). |
373 |
|
store_session(Sid, Socket) -> |
374 |
124 |
mod_bosh_backend:create_session(#bosh_session{sid = Sid, socket = Socket}). |
375 |
|
|
376 |
|
%% MUST be unique and unpredictable |
377 |
|
%% https://xmpp.org/extensions/xep-0124.html#security-sidrid |
378 |
|
%% Also, CETS requires to use node as a part of the key |
379 |
|
%% (but if the key is always random CETS is happy with that too) |
380 |
|
-spec make_sid() -> binary(). |
381 |
|
make_sid() -> |
382 |
124 |
base16:encode(crypto:strong_rand_bytes(20)). |
383 |
|
|
384 |
|
%%-------------------------------------------------------------------- |
385 |
|
%% HTTP errors |
386 |
|
%%-------------------------------------------------------------------- |
387 |
|
|
388 |
|
-spec no_body_error(cowboy_req:req()) -> cowboy_req:req(). |
389 |
|
no_body_error(Req) -> |
390 |
2 |
cowboy_reply(400, ac_all(?DEFAULT_ALLOW_ORIGIN), |
391 |
|
<<"Missing request body">>, Req). |
392 |
|
|
393 |
|
|
394 |
|
-spec method_not_allowed_error(cowboy_req:req()) -> cowboy_req:req(). |
395 |
|
method_not_allowed_error(Req) -> |
396 |
2 |
cowboy_reply(405, ac_all(?DEFAULT_ALLOW_ORIGIN), |
397 |
|
<<"Use POST request method">>, Req). |
398 |
|
|
399 |
|
%%-------------------------------------------------------------------- |
400 |
|
%% BOSH Terminal Binding Error Conditions |
401 |
|
%%-------------------------------------------------------------------- |
402 |
|
|
403 |
|
-spec terminal_condition(binary(), cowboy_req:req()) -> cowboy_req:req(). |
404 |
|
terminal_condition(Condition, Req) -> |
405 |
7 |
terminal_condition(Condition, [], Req). |
406 |
|
|
407 |
|
|
408 |
|
-spec terminal_condition(binary(), [exml:element()], cowboy_req:req()) |
409 |
|
-> cowboy_req:req(). |
410 |
|
terminal_condition(Condition, Details, Req) -> |
411 |
9 |
Body = terminal_condition_body(Condition, Details), |
412 |
9 |
Headers = [content_type()] ++ ac_all(?DEFAULT_ALLOW_ORIGIN), |
413 |
9 |
cowboy_reply(200, Headers, Body, Req). |
414 |
|
|
415 |
|
|
416 |
|
-spec terminal_condition_body(binary(), [exml:element()]) -> binary(). |
417 |
|
terminal_condition_body(Condition, Children) -> |
418 |
9 |
exml:to_binary(#xmlel{name = <<"body">>, |
419 |
|
attrs = [{<<"type">>, <<"terminate">>}, |
420 |
|
{<<"condition">>, Condition}, |
421 |
|
{<<"xmlns">>, ?NS_HTTPBIND}], |
422 |
|
children = Children}). |
423 |
|
|
424 |
|
%%-------------------------------------------------------------------- |
425 |
|
%% Helpers |
426 |
|
%%-------------------------------------------------------------------- |
427 |
|
|
428 |
|
content_type() -> |
429 |
2018 |
{<<"content-type">>, <<"text/xml; charset=utf8">>}. |
430 |
|
|
431 |
|
ac_allow_origin(Origin) -> |
432 |
2022 |
{<<"access-control-allow-origin">>, Origin}. |
433 |
|
|
434 |
|
ac_allow_methods() -> |
435 |
2024 |
{<<"access-control-allow-methods">>, <<"POST, OPTIONS, GET">>}. |
436 |
|
|
437 |
|
ac_allow_headers() -> |
438 |
2024 |
{<<"access-control-allow-headers">>, <<"content-type">>}. |
439 |
|
|
440 |
|
ac_max_age() -> |
441 |
2024 |
{<<"access-control-max-age">>, integer_to_binary(?DEFAULT_MAX_AGE)}. |
442 |
|
|
443 |
|
|
444 |
|
-spec ac_all('undefined' | binary()) -> headers_list(). |
445 |
|
ac_all(Origin) -> |
446 |
15 |
[ac_allow_origin(Origin), |
447 |
|
ac_allow_methods(), |
448 |
|
ac_allow_headers(), |
449 |
|
ac_max_age()]. |
450 |
|
|
451 |
|
-spec bosh_reply_headers() -> headers_list(). |
452 |
|
bosh_reply_headers() -> |
453 |
2007 |
[content_type(), |
454 |
|
ac_allow_origin(?DEFAULT_ALLOW_ORIGIN), |
455 |
|
ac_allow_methods(), |
456 |
|
ac_allow_headers(), |
457 |
|
ac_max_age()]. |
458 |
|
|
459 |
|
set_max_hold(Body) -> |
460 |
126 |
HoldBin = exml_query:attr(Body, <<"hold">>), |
461 |
126 |
ClientHold = binary_to_integer(HoldBin), |
462 |
126 |
maybe_set_max_hold(ClientHold, Body). |
463 |
|
|
464 |
|
|
465 |
|
maybe_set_max_hold(1, Body) -> |
466 |
122 |
{ok, Body}; |
467 |
|
maybe_set_max_hold(ClientHold, #xmlel{attrs = Attrs} = Body) when ClientHold > 1 -> |
468 |
2 |
NewAttrs = lists:keyreplace(<<"hold">>, 1, Attrs, {<<"hold">>, <<"1">>}), |
469 |
2 |
{ok, Body#xmlel{attrs = NewAttrs}}; |
470 |
|
maybe_set_max_hold(_, _) -> |
471 |
2 |
{error, invalid_hold}. |
472 |
|
|
473 |
|
-spec cowboy_reply(non_neg_integer(), headers_list(), binary(), req()) -> req(). |
474 |
|
cowboy_reply(Code, Headers, Body, Req) when is_list(Headers) -> |
475 |
2024 |
cowboy_req:reply(Code, maps:from_list(Headers), Body, Req). |
476 |
|
|
477 |
|
-spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}]. |
478 |
|
config_metrics(HostType) -> |
479 |
210 |
mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]). |