./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
13 -xep([{xep, 206}, {version, "1.4"}]).
14 -xep([{xep, 124}, {version, "1.11"}]).
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/2]).
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, node_cleanup/2, 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}).
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 325 case mod_bosh_socket:is_supervisor_started() of
87 true ->
88 242 ok; % There is only one backend implementation (mnesia), so it is started globally
89 false ->
90 83 mod_bosh_backend:start(Opts),
91 83 {ok, _Pid} = mod_bosh_socket:start_supervisor(),
92 83 ejabberd_hooks:add(node_cleanup, global, ?MODULE, node_cleanup, 50)
93 end.
94
95 -spec stop(mongooseim:host_type()) -> ok.
96 stop(_HostType) ->
97 325 ok.
98
99 -spec config_spec() -> mongoose_config_spec:config_section().
100 config_spec() ->
101 166 #section{items = #{<<"backend">> => #option{type = atom,
102 validate = {module, mod_bosh}},
103 <<"inactivity">> => #option{type = int_or_infinity,
104 validate = positive},
105 <<"max_wait">> => #option{type = int_or_infinity,
106 validate = positive},
107 <<"server_acks">> => #option{type = boolean},
108 <<"max_pause">> => #option{type = integer,
109 validate = positive}
110 },
111 defaults = #{<<"backend">> => mnesia,
112 <<"inactivity">> => 30, % seconds
113 <<"max_wait">> => infinity, % seconds
114 <<"server_acks">> => false,
115 <<"max_pause">> => 120} % seconds
116 }.
117
118 -spec supported_features() -> [atom()].
119 supported_features() ->
120 148 [dynamic_domains].
121
122 %%--------------------------------------------------------------------
123 %% Hooks handlers
124 %%--------------------------------------------------------------------
125
126 node_cleanup(Acc, Node) ->
127
:-(
Res = mod_bosh_backend:node_cleanup(Node),
128
:-(
maps:put(?MODULE, Res, Acc).
129
130 %%--------------------------------------------------------------------
131 %% cowboy_loop_handler callbacks
132 %%--------------------------------------------------------------------
133
134 -spec init(req(), mongoose_http_handler:options()) -> {cowboy_loop, req(), rstate()}.
135 init(Req, _Opts) ->
136 2185 ?LOG_DEBUG(#{what => bosh_init, req => Req}),
137 2185 Msg = init_msg(Req),
138 2185 self() ! Msg,
139 %% Upgrade to cowboy_loop behaviour to enable long polling
140 2185 {cowboy_loop, Req, #rstate{}}.
141
142
143 %% ok return keep the handler looping.
144 %% stop handler is used to reply to the client.
145 -spec info(info(), req(), rstate()) -> {ok, req(), _} | {stop, req(), _}.
146 info(accept_options, Req, State) ->
147 2 Origin = cowboy_req:header(<<"origin">>, Req),
148 2 Headers = ac_all(Origin),
149 2 ?LOG_DEBUG(#{what => bosh_accept_options, headers => Headers,
150 2 text => <<"Handle OPTIONS request in Bosh">>}),
151 2 Req1 = cowboy_reply(200, Headers, <<>>, Req),
152 2 {stop, Req1, State};
153 info(accept_get, Req, State) ->
154 2 Headers = [content_type(),
155 ac_allow_methods(),
156 ac_allow_headers(),
157 ac_max_age()],
158 2 Body = <<"MongooseIM bosh endpoint">>,
159 2 Req1 = cowboy_reply(200, Headers, Body, Req),
160 2 {stop, Req1, State};
161 info(forward_body, Req, S) ->
162 2177 {ok, Body, Req1} = cowboy_req:read_body(Req),
163 %% TODO: the parser should be stored per session,
164 %% but the session is identified inside the to-be-parsed element
165 2177 {ok, BodyElem} = exml:parse(Body),
166 2177 Sid = exml_query:attr(BodyElem, <<"sid">>, <<"missing">>),
167 2177 ?LOG_DEBUG(#{what => bosh_receive, sid => Sid, request_body => Body}),
168 %% Remember req_sid, so it can be used to print a debug message in bosh_reply
169 2177 forward_body(Req1, BodyElem, S#rstate{req_sid = Sid});
170 info({bosh_reply, El}, Req, S) ->
171 2167 BEl = exml:to_binary(El),
172 2167 ?LOG_DEBUG(#{what => bosh_send, req_sid => S#rstate.req_sid, reply_body => BEl,
173 2167 sid => exml_query:attr(El, <<"sid">>, <<"missing">>)}),
174 2167 Headers = bosh_reply_headers(),
175 2167 Req1 = cowboy_reply(200, Headers, BEl, Req),
176 2167 {stop, Req1, S};
177
178 info({close, Sid}, Req, S) ->
179
:-(
?LOG_DEBUG(#{what => bosh_close, sid => Sid}),
180
:-(
Req1 = cowboy_reply(200, [], <<>>, Req),
181
:-(
{stop, Req1, S};
182 info(no_body, Req, State) ->
183 2 ?LOG_DEBUG(#{what => bosh_stop, reason => missing_request_body, req => Req}),
184 2 Req1 = no_body_error(Req),
185 2 {stop, Req1, State};
186 info({wrong_method, Method}, Req, State) ->
187 2 ?LOG_DEBUG(#{what => bosh_stop, reason => wrong_request_method,
188 2 method => Method, req => Req}),
189 2 Req1 = method_not_allowed_error(Req),
190 2 {stop, Req1, State};
191 info(item_not_found, Req, S) ->
192 4 Req1 = terminal_condition(<<"item-not-found">>, Req),
193 4 {stop, Req1, S};
194 info(policy_violation, Req, S) ->
195 2 Req1 = terminal_condition(<<"policy-violation">>, Req),
196 2 {stop, Req1, S}.
197
198
199 terminate(_Reason, _Req, _State) ->
200 2184 ?LOG_DEBUG(#{what => bosh_terminate}),
201 2184 ok.
202
203 %%--------------------------------------------------------------------
204 %% Callbacks implementation
205 %%--------------------------------------------------------------------
206
207 init_msg(Req) ->
208 2185 Method = cowboy_req:method(Req),
209 2185 case Method of
210 <<"OPTIONS">> ->
211 2 accept_options;
212 <<"POST">> ->
213 2179 case cowboy_req:has_body(Req) of
214 true ->
215 2177 forward_body;
216 false ->
217 2 no_body
218 end;
219 <<"GET">> ->
220 2 accept_get;
221 _ ->
222 2 {wrong_method, Method}
223 end.
224
225 -spec to_event_type(exml:element()) -> event_type().
226 to_event_type(Body) ->
227 %% Order of checks is important:
228 %% stream restart has got sid attribute,
229 %% so check for it at the end.
230 2177 check_event_type_streamend(Body).
231
232 check_event_type_streamend(Body) ->
233 2177 case exml_query:attr(Body, <<"type">>) of
234 <<"terminate">> ->
235 124 streamend;
236 _ ->
237 2053 check_event_type_restart(Body)
238 end.
239
240 check_event_type_restart(Body) ->
241 2053 case exml_query:attr(Body, <<"xmpp:restart">>) of
242 <<"true">> ->
243 127 restart;
244 _ ->
245 1926 check_event_type_pause(Body)
246 end.
247
248 check_event_type_pause(Body) ->
249 1926 case exml_query:attr(Body, <<"pause">>) of
250 undefined ->
251 1923 check_event_type_streamstrart(Body);
252 _ ->
253 3 pause
254 end.
255
256 check_event_type_streamstrart(Body) ->
257 1923 case exml_query:attr(Body, <<"sid">>) of
258 undefined ->
259 134 streamstart;
260 _ ->
261 1789 normal
262 end.
263
264 -spec forward_body(req(), exml:element(), rstate())
265 -> {ok, req(), rstate()} | {stop, req(), rstate()}.
266 forward_body(Req, #xmlel{} = Body, S) ->
267 2177 Type = to_event_type(Body),
268 2177 case Type of
269 streamstart ->
270 134 {SessionStarted, Req1} = maybe_start_session(Req, Body),
271 134 case SessionStarted of
272 true ->
273 131 {ok, Req1, S};
274 false ->
275 3 {stop, Req1, S}
276 end;
277 _ ->
278 2043 Sid = exml_query:attr(Body, <<"sid">>),
279 2043 case get_session_socket(Sid) of
280 {ok, Socket} ->
281 %% Forward request from a client to c2s process
282 2043 handle_request(Socket, Type, Body),
283 2043 {ok, Req, S};
284 {error, item_not_found} ->
285
:-(
?LOG_WARNING(#{what => bosh_stop, reason => session_not_found,
286
:-(
sid => Sid}),
287
:-(
Req1 = terminal_condition(<<"item-not-found">>, Req),
288
:-(
{stop, Req1, S}
289 end
290 end.
291
292
293 -spec handle_request(pid(), event_type(), exml:element()) -> ok.
294 handle_request(Socket, EventType, Body) ->
295 2174 mod_bosh_socket:handle_request(Socket, {EventType, self(), Body}).
296
297
298 -spec get_session_socket(mod_bosh:sid()) -> {ok, pid()} | {error, item_not_found}.
299 get_session_socket(Sid) ->
300 2043 case mod_bosh_backend:get_session(Sid) of
301 [BS] ->
302 2043 {ok, BS#bosh_session.socket};
303 [] ->
304
:-(
{error, item_not_found}
305 end.
306
307
308 -spec maybe_start_session(req(), exml:element()) ->
309 {SessionStarted :: boolean(), req()}.
310 maybe_start_session(Req, Body) ->
311 134 Domain = exml_query:attr(Body, <<"to">>),
312 134 case mongoose_domain_api:get_domain_host_type(Domain) of
313 {ok, HostType} ->
314 134 case gen_mod:is_loaded(HostType, ?MODULE) of
315 true ->
316 133 maybe_start_session_on_known_host(HostType, Req, Body);
317 false ->
318 1 {false, terminal_condition(<<"host-unknown">>, Req)}
319 end;
320 {error, not_found} ->
321
:-(
{false, terminal_condition(<<"host-unknown">>, Req)}
322 end.
323
324 -spec maybe_start_session_on_known_host(mongooseim:host_type(), req(), exml:element()) ->
325 {SessionStarted :: boolean(), req()}.
326 maybe_start_session_on_known_host(HostType, Req, Body) ->
327 133 try
328 133 maybe_start_session_on_known_host_unsafe(HostType, Req, Body)
329 catch
330 error:Reason ->
331 %% It's here because something catch-y was here before
332 2 ?LOG_ERROR(#{what => bosh_stop, issue => undefined_condition,
333
:-(
reason => Reason}),
334 2 Req1 = terminal_condition(<<"undefined-condition">>, [], Req),
335 2 {false, Req1}
336 end.
337
338 -spec maybe_start_session_on_known_host_unsafe(mongooseim:host_type(), req(), exml:element()) ->
339 {SessionStarted :: boolean(), req()}.
340 maybe_start_session_on_known_host_unsafe(HostType, Req, Body) ->
341 %% Version isn't checked as it would be meaningless when supporting
342 %% only a subset of the specification.
343 133 {ok, NewBody} = set_max_hold(Body),
344 131 Peer = cowboy_req:peer(Req),
345 131 PeerCert = cowboy_req:cert(Req),
346 131 start_session(HostType, Peer, PeerCert, NewBody),
347 131 {true, Req}.
348
349 -spec start_session(mongooseim:host_type(), mongoose_transport:peer(),
350 binary() | undefined, exml:element()) -> any().
351 start_session(HostType, Peer, PeerCert, Body) ->
352 131 Sid = make_sid(),
353 131 {ok, Socket} = mod_bosh_socket:start(HostType, Sid, Peer, PeerCert),
354 131 store_session(Sid, Socket),
355 131 handle_request(Socket, streamstart, Body),
356 131 ?LOG_DEBUG(#{what => bosh_start_session, sid => Sid}).
357
358 -spec store_session(Sid :: sid(), Socket :: pid()) -> any().
359 store_session(Sid, Socket) ->
360 131 mod_bosh_backend:create_session(#bosh_session{sid = Sid, socket = Socket}).
361
362 -spec make_sid() -> binary().
363 make_sid() ->
364 131 sha:sha1_hex(term_to_binary(make_ref())).
365
366 %%--------------------------------------------------------------------
367 %% HTTP errors
368 %%--------------------------------------------------------------------
369
370 -spec no_body_error(cowboy_req:req()) -> cowboy_req:req().
371 no_body_error(Req) ->
372 2 cowboy_reply(400, ac_all(?DEFAULT_ALLOW_ORIGIN),
373 <<"Missing request body">>, Req).
374
375
376 -spec method_not_allowed_error(cowboy_req:req()) -> cowboy_req:req().
377 method_not_allowed_error(Req) ->
378 2 cowboy_reply(405, ac_all(?DEFAULT_ALLOW_ORIGIN),
379 <<"Use POST request method">>, Req).
380
381 %%--------------------------------------------------------------------
382 %% BOSH Terminal Binding Error Conditions
383 %%--------------------------------------------------------------------
384
385 -spec terminal_condition(binary(), cowboy_req:req()) -> cowboy_req:req().
386 terminal_condition(Condition, Req) ->
387 7 terminal_condition(Condition, [], Req).
388
389
390 -spec terminal_condition(binary(), [exml:element()], cowboy_req:req())
391 -> cowboy_req:req().
392 terminal_condition(Condition, Details, Req) ->
393 9 Body = terminal_condition_body(Condition, Details),
394 9 Headers = [content_type()] ++ ac_all(?DEFAULT_ALLOW_ORIGIN),
395 9 cowboy_reply(200, Headers, Body, Req).
396
397
398 -spec terminal_condition_body(binary(), [exml:element()]) -> binary().
399 terminal_condition_body(Condition, Children) ->
400 9 exml:to_binary(#xmlel{name = <<"body">>,
401 attrs = [{<<"type">>, <<"terminate">>},
402 {<<"condition">>, Condition},
403 {<<"xmlns">>, ?NS_HTTPBIND}],
404 children = Children}).
405
406 %%--------------------------------------------------------------------
407 %% Helpers
408 %%--------------------------------------------------------------------
409
410 content_type() ->
411 2178 {<<"content-type">>, <<"text/xml; charset=utf8">>}.
412
413 ac_allow_origin(Origin) ->
414 2182 {<<"access-control-allow-origin">>, Origin}.
415
416 ac_allow_methods() ->
417 2184 {<<"access-control-allow-methods">>, <<"POST, OPTIONS, GET">>}.
418
419 ac_allow_headers() ->
420 2184 {<<"access-control-allow-headers">>, <<"content-type">>}.
421
422 ac_max_age() ->
423 2184 {<<"access-control-max-age">>, integer_to_binary(?DEFAULT_MAX_AGE)}.
424
425
426 -spec ac_all('undefined' | binary()) -> headers_list().
427 ac_all(Origin) ->
428 15 [ac_allow_origin(Origin),
429 ac_allow_methods(),
430 ac_allow_headers(),
431 ac_max_age()].
432
433 -spec bosh_reply_headers() -> headers_list().
434 bosh_reply_headers() ->
435 2167 [content_type(),
436 ac_allow_origin(?DEFAULT_ALLOW_ORIGIN),
437 ac_allow_methods(),
438 ac_allow_headers(),
439 ac_max_age()].
440
441 set_max_hold(Body) ->
442 133 HoldBin = exml_query:attr(Body, <<"hold">>),
443 133 ClientHold = binary_to_integer(HoldBin),
444 133 maybe_set_max_hold(ClientHold, Body).
445
446
447 maybe_set_max_hold(1, Body) ->
448 129 {ok, Body};
449 maybe_set_max_hold(ClientHold, #xmlel{attrs = Attrs} = Body) when ClientHold > 1 ->
450 2 NewAttrs = lists:keyreplace(<<"hold">>, 1, Attrs, {<<"hold">>, <<"1">>}),
451 2 {ok, Body#xmlel{attrs = NewAttrs}};
452 maybe_set_max_hold(_, _) ->
453 2 {error, invalid_hold}.
454
455 -spec cowboy_reply(non_neg_integer(), headers_list(), binary(), req()) -> req().
456 cowboy_reply(Code, Headers, Body, Req) when is_list(Headers) ->
457 2184 cowboy_req:reply(Code, maps:from_list(Headers), Body, Req).
458
459 -spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}].
460 config_metrics(HostType) ->
461 192 mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]).
Line Hits Source