./ct_report/coverage/mod_bosh.COVER.html

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