./ct_report/coverage/jingle_sip_callbacks.COVER.html

1 %% @doc
2 %% This module defines callbacks for nksip application
3 %% Exported functions whill be called when there is new message to MongooseIM
4 %% or when there is an response to a SIP INVITE sent from MongooseIM to a SIP Proxy
5 %% @author Michal Piotrowski <michal.piotrowski@erlang-solutions.com>
6 %%
7 %%==============================================================================
8 %% Copyright 2018 Erlang Solutions Ltd.
9 %%
10 %% Licensed under the Apache License, Version 2.0 (the "License");
11 %% you may not use this file except in compliance with the License.
12 %% You may obtain a copy of the License at
13 %%
14 %% http://www.apache.org/licenses/LICENSE-2.0
15 %%
16 %% Unless required by applicable law or agreed to in writing, software
17 %% distributed under the License is distributed on an "AS IS" BASIS,
18 %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 %% See the License for the specific language governing permissions and
20 %% limitations under the License.
21 %%==============================================================================
22 -module(jingle_sip_callbacks).
23
24 -include("mongoose.hrl").
25 -include("jlib.hrl").
26 -include_lib("nksip/include/nksip.hrl").
27 -include_lib("nksip/include/nksip_call.hrl").
28
29 %% this is because nksip has wrong type specs
30 -dialyzer({nowarn_function, [sip_invite_unsafe/2,
31 sip_reinvite_unsafe/2,
32 sip_bye/2,
33 sip_cancel/3,
34 send_ringing_session_info/2,
35 invite_resp_callback/1
36 ]}).
37
38 %% SIP callbacks
39 -export([sip_invite/2]).
40 -export([sip_reinvite/2]).
41 -export([sip_info/2]).
42 -export([sip_bye/2]).
43 -export([sip_cancel/3]).
44 -export([sip_dialog_update/3]).
45 -export([invite_resp_callback/1]).
46
47 -ignore_xref([sip_bye/2, sip_cancel/3, sip_dialog_update/3, sip_info/2,
48 sip_invite/2, sip_reinvite/2]).
49
50 %% nksip specs do not fully cover case when they return a parsing error
51 -dialyzer({nowarn_function, [assert_sdp_record/2]}).
52
53 sip_invite(Req, Call) ->
54 16 try
55 16 sip_invite_unsafe(Req, Call)
56 catch Class:Reason:StackTrace ->
57
:-(
?LOG_WARNING(#{what => sip_invite_failed,
58 text => <<"Error parsing sip invite">>, sip_req => Req,
59
:-(
class => Class, reason => Reason, stacktrace => StackTrace}),
60
:-(
{error, request_not_parsable}
61 end.
62
63 sip_reinvite(Req, Call) ->
64 4 try
65 4 sip_reinvite_unsafe(Req, Call)
66 catch Class:Reason:StackTrace ->
67
:-(
?LOG_WARNING(#{what => sip_reinvite_failed,
68 text => <<"Error parsing sip reinvite">>, sip_req => Req,
69
:-(
class => Class, reason => Reason, stacktrace => StackTrace}),
70
:-(
{error, request_not_parsable}
71 end.
72
73
74 sip_invite_unsafe(Req, _Call) ->
75 16 {FromJID, FromBinary} = get_user_from_sip_msg(from, Req),
76 16 {ToJID, ToBinary} = get_user_from_sip_msg(to, Req),
77
78 16 case ejabberd_sm:is_offline(ToJID) of
79 false ->
80 15 translate_and_deliver_invite(Req, FromJID, FromBinary, ToJID, ToBinary);
81 _ ->
82 1 CallID = nksip_sipmsg:header(<<"call-id">>, Req),
83 1 ?LOG_INFO(#{what => sip_invite, reply => temporarily_unavailable,
84 text => <<"Got SIP INVITE from NkSIP, but destination user is offline">>,
85 from_jid => FromBinary, to_jid => ToBinary,
86 1 call_id => CallID, sip_req => Req}),
87 1 {reply, temporarily_unavailable}
88 end.
89
90 translate_and_deliver_invite(Req, FromJID, FromBinary, ToJID, ToBinary) ->
91 15 CallID = nksip_sipmsg:header(<<"call-id">>, Req),
92 15 Body = nksip_sipmsg:meta(body, Req),
93
94 15 {ok, ReqID} = nksip_request:get_handle(Req),
95
96 15 {CodecMap, SDP} = nksip_sdp_util:extract_codec_map(Body),
97 15 assert_sdp_record(Body, SDP),
98
99 15 OtherEls = sip_to_jingle:parse_sdp_attributes(SDP#sdp.attributes),
100
101 15 ContentEls = [sip_to_jingle:sdp_media_to_content_el(Media, CodecMap) || Media <- SDP#sdp.medias],
102
103 15 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-initiate">>, ContentEls ++ OtherEls),
104
105 15 ok = mod_jingle_sip_session:set_incoming_request(CallID, ReqID, FromJID, ToJID, JingleEl),
106
107 15 ?LOG_INFO(#{what => sip_invite, text => <<"Got SIP INVITE from NkSIP">>,
108 from_jid => FromBinary, to_jid => ToBinary,
109 15 call_id => CallID, sip_req => Req}),
110
111 15 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
112 15 Acc = mongoose_acc:new(#{ location => ?LOCATION,
113 lserver => FromJID#jid.lserver,
114 element => IQEl,
115 from_jid => FromJID,
116 to_jid => ToJID }),
117 15 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
118
119 15 {reply, ringing}.
120
121 sip_reinvite_unsafe(Req, _Call) ->
122 4 ?LOG_INFO(#{what => sip_reinvite, sip_req => Req}),
123 4 {FromJID, FromBinary} = get_user_from_sip_msg(from, Req),
124 4 {ToJID, ToBinary} = get_user_from_sip_msg(to, Req),
125
126 4 CallID = nksip_sipmsg:header(<<"call-id">>, Req),
127 4 Body = nksip_sipmsg:meta(body, Req),
128
129 4 {CodecMap, SDP} = nksip_sdp_util:extract_codec_map(Body),
130 4 assert_sdp_record(Body, SDP),
131 4 RemainingAttrs = SDP#sdp.attributes,
132 4 OtherEls = sip_to_jingle:parse_sdp_attributes(RemainingAttrs),
133
134 4 ContentEls = [sip_to_jingle:sdp_media_to_content_el(Media, CodecMap) || Media <- SDP#sdp.medias],
135 4 Name = get_action_name_from_sdp(RemainingAttrs, <<"transport-info">>),
136 4 JingleEl = jingle_sip_helper:jingle_element(CallID, Name, ContentEls ++ OtherEls),
137
138 4 ?LOG_INFO(#{what => sip_reinvite, text => <<"Got SIP re-INVITE from NkSIP">>,
139 from_jid => FromBinary, to_jid => ToBinary,
140 4 call_id => CallID, sip_req => Req}),
141
142 4 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
143 4 Acc = mongoose_acc:new(#{ location => ?LOCATION,
144 lserver => FromJID#jid.lserver,
145 element => IQEl,
146 from_jid => FromJID,
147 to_jid => ToJID }),
148 4 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
149 4 {reply, ok}.
150
151 get_action_name_from_sdp(Attrs, Default) ->
152 4 case lists:keyfind(<<"jingle-action">>, 1, Attrs) of
153 {_, [Name]} ->
154 3 Name;
155 _ ->
156 1 Default
157 end.
158
159
160 sip_info(Req, _Call) ->
161
:-(
?LOG_INFO(#{what => sip_info, sip_req => Req}),
162
:-(
noreply.
163
164 sip_bye(Req, _Call) ->
165 12 {FromJID, FromBinary} = get_user_from_sip_msg(from, Req),
166 12 {ToJID, ToBinary} = get_user_from_sip_msg(to, Req),
167
168 12 CallID = nksip_sipmsg:header(<<"call-id">>, Req),
169 12 ReasonEl = #xmlel{name = <<"reason">>,
170 children = [#xmlel{name = <<"success">>}]},
171 12 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-terminate">>, [ReasonEl]),
172 12 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
173 12 Acc = mongoose_acc:new(#{ location => ?LOCATION,
174 lserver => FromJID#jid.lserver,
175 element => IQEl,
176 from_jid => FromJID,
177 to_jid => ToJID }),
178 12 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
179 12 ok = mod_jingle_sip_session:remove_session(CallID),
180 12 {reply, ok}.
181
182 sip_cancel(_InviteReq, Req, _Call) ->
183 1 {FromJID, FromBinary} = get_user_from_sip_msg(from, Req),
184 1 {ToJID, ToBinary} = get_user_from_sip_msg(to, Req),
185
186 1 CallID = nksip_sipmsg:header(<<"call-id">>, Req),
187 1 ReasonEl = #xmlel{name = <<"reason">>,
188 children = [#xmlel{name = <<"decline">>}]},
189 1 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-terminate">>, [ReasonEl]),
190 1 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
191 1 Acc = mongoose_acc:new(#{ location => ?LOCATION,
192 lserver => FromJID#jid.lserver,
193 element => IQEl,
194 from_jid => FromJID,
195 to_jid => ToJID }),
196 1 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
197 1 ok = mod_jingle_sip_session:remove_session(CallID),
198 1 {reply, ok}.
199
200 sip_dialog_update(start, Dialog, Call) ->
201 32 {ok, DialogHandle} = nksip_dialog:get_handle(Dialog),
202 32 [Transaction | _] = Call#call.trans,
203 32 case Transaction#trans.class of
204 uas ->
205 15 {ok, CallID} = nksip_dialog:call_id(Dialog),
206 15 mod_jingle_sip_session:set_incoming_handle(CallID, DialogHandle);
207
208 _ ->
209 17 ok
210 end,
211 32 noreply;
212 sip_dialog_update(_, _, _) ->
213 157 noreply.
214
215 %% @doc
216 %% This function is called for every response to the SIP INVITE
217 %% SIP response contains the same headers as request
218 %% That's why we need to switch `from' and `to' when preparing and routing Jingle
219 %% to the request originator
220 %% interpreted status codes:
221 %% * 180 and 183 - provisional respons - we can send `ringing' session-info
222 %% * 200 - the invite was accepted we can sent `session-accepted' stanza
223 %% * 487 - this is to confirm INVITE cancelation from the other side (no action in this case)
224 %% * 603 - used to decline the INVITE by the reciving side
225 %% * all error responses between 400 and 700 result in genering session-terminate reason
226 invite_resp_callback({resp, 180, SIPMsg, _Call}) ->
227 15 send_ringing_session_info(SIPMsg, 180);
228 invite_resp_callback({resp, 183, SIPMsg, _Call}) ->
229 2 send_ringing_session_info(SIPMsg, 183);
230 invite_resp_callback({resp, 200, SIPMsg, _Call}) ->
231 11 {ToJID, ToBinary} = get_user_from_sip_msg(from, SIPMsg),
232 11 {FromJID, FromBinary} = get_user_from_sip_msg(to, SIPMsg),
233
234 11 Body = nksip_sipmsg:meta(body, SIPMsg),
235 11 CallID = nksip_sipmsg:header(<<"call-id">>, SIPMsg),
236 11 {CodecMap, SDP} = nksip_sdp_util:extract_codec_map(Body),
237 11 assert_sdp_record(Body, SDP),
238 11 OtherEls = sip_to_jingle:parse_sdp_attributes(SDP#sdp.attributes),
239
240
241 11 ContentEls = [sip_to_jingle:sdp_media_to_content_el(Media, CodecMap) || Media <- SDP#sdp.medias],
242
243 11 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-accept">>, ContentEls ++ OtherEls),
244 11 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
245 11 Acc = mongoose_acc:new(#{ location => ?LOCATION,
246 lserver => FromJID#jid.lserver,
247 element => IQEl,
248 from_jid => FromJID,
249 to_jid => ToJID }),
250 11 ok = mod_jingle_sip_session:set_outgoing_accepted(CallID),
251 11 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
252 11 ok;
253 invite_resp_callback({resp, 487, _SIPMsg, _Call}) ->
254 %% this error response only confirms that that the transaction was canceled
255 %% the real `session-terminate` stanza is sent by `sip_cancel/3` callback
256 1 ok;
257 invite_resp_callback({resp, 486, SIPMsg, _Call}) ->
258 2 {ToJID, ToBinary} = get_user_from_sip_msg(from, SIPMsg),
259 2 {FromJID, FromBinary} = get_user_from_sip_msg(to, SIPMsg),
260 2 CallID = nksip_sipmsg:header(<<"call-id">>, SIPMsg),
261
262 2 ReasonEl = #xmlel{name = <<"reason">>,
263 children = [#xmlel{name = <<"decline">>}]},
264 2 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-terminate">>, [ReasonEl]),
265 2 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
266 2 Acc = mongoose_acc:new(#{ location => ?LOCATION,
267 lserver => FromJID#jid.lserver,
268 element => IQEl,
269 from_jid => FromJID,
270 to_jid => ToJID }),
271 2 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
272 2 ok;
273 invite_resp_callback({resp, ErrorCode, SIPMsg, _Call})
274 when ErrorCode >= 400, ErrorCode =< 700 ->
275 4 {ToJID, ToBinary} = get_user_from_sip_msg(from, SIPMsg),
276 4 {FromJID, FromBinary} = get_user_from_sip_msg(to, SIPMsg),
277 4 CallID = nksip_sipmsg:header(<<"call-id">>, SIPMsg),
278
279 4 ReasonEl = make_session_terminate_reason_el(ErrorCode, SIPMsg),
280
281 4 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-terminate">>, [ReasonEl]),
282 4 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
283 4 Acc = mongoose_acc:new(#{ location => ?LOCATION,
284 lserver => FromJID#jid.lserver,
285 element => IQEl,
286 from_jid => FromJID,
287 to_jid => ToJID }),
288 4 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
289 4 ok = mod_jingle_sip_session:remove_session(CallID),
290 4 ok;
291 invite_resp_callback(Data) ->
292
:-(
?LOG_ERROR(#{what => sip_unknown_response, sip_data => Data}).
293
294 send_ringing_session_info(SIPMsg, ErrorCode) ->
295 %% SIP response contains the same headers as request
296 %% That's why we need to switch `from` and `to` when preparing Jingle packet
297 17 {ToJID, ToBinary} = get_user_from_sip_msg(from, SIPMsg),
298 17 {FromJID, FromBinary} = get_user_from_sip_msg(to, SIPMsg),
299
300 17 DialogHandle = nksip_sipmsg:meta(dialog_handle, SIPMsg),
301 17 {SrvId, DialogId, CallID} = nksip_dialog_lib:parse_handle(DialogHandle),
302 17 ?LOG_INFO(#{what => sip_invite_resp_callback, error_code => ErrorCode,
303 call_id => CallID, sip_req => SIPMsg,
304 dialog_id => DialogId, server_id => SrvId,
305 17 from_jid => FromBinary, to_binary => ToBinary}),
306
307 17 mod_jingle_sip_session:set_outgoing_handle(CallID, DialogHandle, FromJID, ToJID),
308
309 17 RingingEl = #xmlel{name = <<"ringing">>,
310 attrs = [{<<"xmlns">>, <<"urn:xmpp:jingle:apps:rtp:info:1">>}]},
311 17 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-info">>, [RingingEl]),
312 17 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
313 17 Acc = mongoose_acc:new(#{ location => ?LOCATION,
314 lserver => FromJID#jid.lserver,
315 element => IQEl,
316 from_jid => FromJID,
317 to_jid => ToJID }),
318 17 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
319 17 ok.
320
321 get_user_from_sip_msg(Field, SIPMsg) ->
322 134 URI = nksip_sipmsg:meta(Field, SIPMsg),
323 134 #uri{user = ToUserIn, domain = ToDomain, path = ToPath} = URI,
324
325 134 Resource = path_to_res(ToPath),
326
327 134 ToUser = jingle_sip_helper:maybe_rewrite_from_phone(ToDomain, ToUserIn),
328
329 134 ToJID = jid:make(ToUser, ToDomain, Resource),
330 134 {ToJID, jid:to_binary({ToUser, ToDomain, Resource})}.
331
332 path_to_res(<<"/", Rest/binary>>) ->
333
:-(
Rest;
334 path_to_res(Other) ->
335 134 Other.
336
337 make_session_terminate_reason_el(ErrorCode, #sipmsg{class = {resp, ErrorCode, Binary}}) ->
338 4 Reason = #xmlel{name = <<"general-error">>},
339 4 Details = #xmlel{name = <<"sip-error">>,
340 attrs = [{<<"code">>, integer_to_binary(ErrorCode)}],
341 children = [#xmlcdata{content = Binary}]},
342 4 #xmlel{name = <<"reason">>,
343 children = [Reason, Details]}.
344
345 maybe_route_to_all_sessions(From, To, Acc, Packet) ->
346 66 PResources = ejabberd_sm:get_user_present_resources(To),
347 66 lists:foreach(
348 fun({_, R}) ->
349 55 ejabberd_router:route(From, jid:replace_resource(To, R), Acc, Packet)
350 end, PResources).
351
352
353 assert_sdp_record(_Body, #sdp{}) ->
354 30 ok;
355 assert_sdp_record(Body, SDP) ->
356
:-(
error({assert_sdp_record, Body, SDP}).
Line Hits Source