./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
15 %% gen_mod callbacks
16 -export([start/2,
17 stop/1,
18 config_spec/0,
19 supported_features/0]).
20
21 %% cowboy_loop_handler callbacks
22 -export([init/2,
23 info/3,
24 terminate/3]).
25
26 %% Hooks callbacks
27 -export([node_cleanup/2]).
28
29 %% For testing and debugging
30 -export([get_session_socket/1, store_session/2]).
31
32 -export([config_metrics/1]).
33
34 -ignore_xref([get_session_socket/1, node_cleanup/2, store_session/2]).
35
36 -include("mongoose.hrl").
37 -include("jlib.hrl").
38 -include_lib("exml/include/exml_stream.hrl").
39 -include("mod_bosh.hrl").
40 -include("mongoose_config_spec.hrl").
41
42 -define(DEFAULT_MAX_AGE, 1728000). %% 20 days in seconds
43 -define(DEFAULT_ALLOW_ORIGIN, <<"*">>).
44
45 -export_type([session/0,
46 sid/0,
47 event_type/0,
48 socket/0
49 ]).
50
51 -type socket() :: #bosh_socket{}.
52 -type session() :: #bosh_session{
53 sid :: mod_bosh:sid(),
54 socket :: pid()
55 }.
56 -type sid() :: binary().
57 -type event_type() :: streamstart
58 | restart
59 | normal
60 | pause
61 | streamend.
62
63 -type headers_list() :: [{binary(), binary()}].
64
65 %% Request State
66 -record(rstate, {req_sid}).
67 -type rstate() :: #rstate{}.
68 -type req() :: cowboy_req:req().
69
70 -type info() :: accept_options
71 | accept_get
72 | item_not_found
73 | no_body
74 | policy_violation
75 | {bosh_reply, exml:element()}
76 | {close, _}
77 | {wrong_method, _}.
78
79 %%--------------------------------------------------------------------
80 %% gen_mod callbacks
81 %%--------------------------------------------------------------------
82
83 -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
84 start(_HostType, Opts) ->
85 302 case mod_bosh_socket:is_supervisor_started() of
86 true ->
87 226 ok; % There is only one backend implementation (mnesia), so it is started globally
88 false ->
89 76 mod_bosh_backend:start(Opts),
90 76 {ok, _Pid} = mod_bosh_socket:start_supervisor(),
91 76 ejabberd_hooks:add(node_cleanup, global, ?MODULE, node_cleanup, 50)
92 end.
93
94 -spec stop(mongooseim:host_type()) -> ok.
95 stop(_HostType) ->
96 302 ok.
97
98 -spec config_spec() -> mongoose_config_spec:config_section().
99 config_spec() ->
100 152 #section{items = #{<<"backend">> => #option{type = atom,
101 validate = {module, mod_bosh}},
102 <<"inactivity">> => #option{type = int_or_infinity,
103 validate = positive},
104 <<"max_wait">> => #option{type = int_or_infinity,
105 validate = positive},
106 <<"server_acks">> => #option{type = boolean},
107 <<"max_pause">> => #option{type = integer,
108 validate = positive}
109 },
110 defaults = #{<<"backend">> => mnesia,
111 <<"inactivity">> => 30, % seconds
112 <<"max_wait">> => infinity, % seconds
113 <<"server_acks">> => false,
114 <<"max_pause">> => 120}, % seconds
115 format_items = map}.
116
117 -spec supported_features() -> [atom()].
118 supported_features() ->
119 145 [dynamic_domains].
120
121 %%--------------------------------------------------------------------
122 %% Hooks handlers
123 %%--------------------------------------------------------------------
124
125 node_cleanup(Acc, Node) ->
126
:-(
Res = mod_bosh_backend:node_cleanup(Node),
127
:-(
maps:put(?MODULE, Res, Acc).
128
129 %%--------------------------------------------------------------------
130 %% cowboy_loop_handler callbacks
131 %%--------------------------------------------------------------------
132
133 -type option() :: {atom(), any()}.
134 -spec init(req(), _Opts :: [option()]) -> {cowboy_loop, req(), rstate()}.
135 init(Req, _Opts) ->
136 2179 ?LOG_DEBUG(#{what => bosh_init, req => Req}),
137 2179 Msg = init_msg(Req),
138 2179 self() ! Msg,
139 %% Upgrade to cowboy_loop behaviour to enable long polling
140 2179 {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 2171 {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 2171 {ok, BodyElem} = exml:parse(Body),
166 2171 Sid = exml_query:attr(BodyElem, <<"sid">>, <<"missing">>),
167 2171 ?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 2171 forward_body(Req1, BodyElem, S#rstate{req_sid = Sid});
170 info({bosh_reply, El}, Req, S) ->
171 2160 BEl = exml:to_binary(El),
172 2160 ?LOG_DEBUG(#{what => bosh_send, req_sid => S#rstate.req_sid, reply_body => BEl,
173 2160 sid => exml_query:attr(El, <<"sid">>, <<"missing">>)}),
174 2160 Headers = bosh_reply_headers(),
175 2160 Req1 = cowboy_reply(200, Headers, BEl, Req),
176 2160 {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 2178 ?LOG_DEBUG(#{what => bosh_terminate}),
201 2178 ok.
202
203 %%--------------------------------------------------------------------
204 %% Callbacks implementation
205 %%--------------------------------------------------------------------
206
207 init_msg(Req) ->
208 2179 Method = cowboy_req:method(Req),
209 2179 case Method of
210 <<"OPTIONS">> ->
211 2 accept_options;
212 <<"POST">> ->
213 2173 case cowboy_req:has_body(Req) of
214 true ->
215 2171 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 2171 check_event_type_streamend(Body).
231
232 check_event_type_streamend(Body) ->
233 2171 case exml_query:attr(Body, <<"type">>) of
234 <<"terminate">> ->
235 122 streamend;
236 _ ->
237 2049 check_event_type_restart(Body)
238 end.
239
240 check_event_type_restart(Body) ->
241 2049 case exml_query:attr(Body, <<"xmpp:restart">>) of
242 <<"true">> ->
243 125 restart;
244 _ ->
245 1924 check_event_type_pause(Body)
246 end.
247
248 check_event_type_pause(Body) ->
249 1924 case exml_query:attr(Body, <<"pause">>) of
250 undefined ->
251 1921 check_event_type_streamstrart(Body);
252 _ ->
253 3 pause
254 end.
255
256 check_event_type_streamstrart(Body) ->
257 1921 case exml_query:attr(Body, <<"sid">>) of
258 undefined ->
259 132 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 2171 Type = to_event_type(Body),
268 2171 case Type of
269 streamstart ->
270 132 {SessionStarted, Req1} = maybe_start_session(Req, Body),
271 132 case SessionStarted of
272 true ->
273 129 {ok, Req1, S};
274 false ->
275 3 {stop, Req1, S}
276 end;
277 _ ->
278 2039 Sid = exml_query:attr(Body, <<"sid">>),
279 2039 case get_session_socket(Sid) of
280 {ok, Socket} ->
281 %% Forward request from a client to c2s process
282 2038 handle_request(Socket, Type, Body),
283 2038 {ok, Req, S};
284 {error, item_not_found} ->
285 1 ?LOG_WARNING(#{what => bosh_stop, reason => session_not_found,
286
:-(
sid => Sid}),
287 1 Req1 = terminal_condition(<<"item-not-found">>, Req),
288 1 {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 2167 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 2039 case mod_bosh_backend:get_session(Sid) of
301 [BS] ->
302 2038 {ok, BS#bosh_session.socket};
303 [] ->
304 1 {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 132 Domain = exml_query:attr(Body, <<"to">>),
312 132 case mongoose_domain_api:get_domain_host_type(Domain) of
313 {ok, HostType} ->
314 132 case gen_mod:is_loaded(HostType, ?MODULE) of
315 true ->
316 131 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 131 try
328 131 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 131 {ok, NewBody} = set_max_hold(Body),
344 129 Peer = cowboy_req:peer(Req),
345 129 PeerCert = cowboy_req:cert(Req),
346 129 start_session(HostType, Peer, PeerCert, NewBody),
347 129 {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 129 Sid = make_sid(),
353 129 {ok, Socket} = mod_bosh_socket:start(HostType, Sid, Peer, PeerCert),
354 129 store_session(Sid, Socket),
355 129 handle_request(Socket, streamstart, Body),
356 129 ?LOG_DEBUG(#{what => bosh_start_session, sid => Sid}).
357
358 -spec store_session(Sid :: sid(), Socket :: pid()) -> any().
359 store_session(Sid, Socket) ->
360 129 mod_bosh_backend:create_session(#bosh_session{sid = Sid, socket = Socket}).
361
362 -spec make_sid() -> binary().
363 make_sid() ->
364 129 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 8 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 10 Body = terminal_condition_body(Condition, Details),
394 10 Headers = [content_type()] ++ ac_all(?DEFAULT_ALLOW_ORIGIN),
395 10 cowboy_reply(200, Headers, Body, Req).
396
397
398 -spec terminal_condition_body(binary(), [exml:element()]) -> binary().
399 terminal_condition_body(Condition, Children) ->
400 10 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 2172 {<<"content-type">>, <<"text/xml; charset=utf8">>}.
412
413 ac_allow_origin(Origin) ->
414 2176 {<<"access-control-allow-origin">>, Origin}.
415
416 ac_allow_methods() ->
417 2178 {<<"access-control-allow-methods">>, <<"POST, OPTIONS, GET">>}.
418
419 ac_allow_headers() ->
420 2178 {<<"access-control-allow-headers">>, <<"content-type">>}.
421
422 ac_max_age() ->
423 2178 {<<"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 16 [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 2160 [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 131 HoldBin = exml_query:attr(Body, <<"hold">>),
443 131 ClientHold = binary_to_integer(HoldBin),
444 131 maybe_set_max_hold(ClientHold, Body).
445
446
447 maybe_set_max_hold(1, Body) ->
448 127 {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 2178 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 188 mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]).
Line Hits Source