./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 sip_invite(Req, Call) ->
51 16 try
52 16 sip_invite_unsafe(Req, Call)
53 catch Class:Reason:StackTrace ->
54
:-(
?LOG_WARNING(#{what => sip_invite_failed,
55 text => <<"Error parsing sip invite">>, sip_req => Req,
56
:-(
class => Class, reason => Reason, stacktrace => StackTrace}),
57
:-(
{error, request_not_parsable}
58 end.
59
60 sip_reinvite(Req, Call) ->
61 4 try
62 4 sip_reinvite_unsafe(Req, Call)
63 catch Class:Reason:StackTrace ->
64
:-(
?LOG_WARNING(#{what => sip_reinvite_failed,
65 text => <<"Error parsing sip reinvite">>, sip_req => Req,
66
:-(
class => Class, reason => Reason, stacktrace => StackTrace}),
67
:-(
{error, request_not_parsable}
68 end.
69
70
71 sip_invite_unsafe(Req, _Call) ->
72 16 {FromJID, FromBinary} = get_user_from_sip_msg(from, Req),
73 16 {ToJID, ToBinary} = get_user_from_sip_msg(to, Req),
74
75 16 case ejabberd_sm:is_offline(ToJID) of
76 false ->
77 15 translate_and_deliver_invite(Req, FromJID, FromBinary, ToJID, ToBinary);
78 _ ->
79 1 CallID = nksip_sipmsg:header(<<"call-id">>, Req),
80 1 ?LOG_INFO(#{what => sip_invite, reply => temporarily_unavailable,
81 text => <<"Got SIP INVITE from NkSIP, but destination user is offline">>,
82 from_jid => FromBinary, to_jid => ToBinary,
83 1 call_id => CallID, sip_req => Req}),
84 1 {reply, temporarily_unavailable}
85 end.
86
87 translate_and_deliver_invite(Req, FromJID, FromBinary, ToJID, ToBinary) ->
88 15 CallID = nksip_sipmsg:header(<<"call-id">>, Req),
89 15 Body = nksip_sipmsg:meta(body, Req),
90
91 15 {ok, ReqID} = nksip_request:get_handle(Req),
92
93 15 {CodecMap, SDP} = nksip_sdp_util:extract_codec_map(Body),
94
95 15 OtherEls = sip_to_jingle:parse_sdp_attributes(SDP#sdp.attributes),
96
97 15 ContentEls = [sip_to_jingle:sdp_media_to_content_el(Media, CodecMap) || Media <- SDP#sdp.medias],
98
99 15 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-initiate">>, ContentEls ++ OtherEls),
100
101 15 ok = mod_jingle_sip_backend:set_incoming_request(CallID, ReqID, FromJID, ToJID, JingleEl),
102
103 15 ?LOG_INFO(#{what => sip_invite, text => <<"Got SIP INVITE from NkSIP">>,
104 from_jid => FromBinary, to_jid => ToBinary,
105 15 call_id => CallID, sip_req => Req}),
106
107 15 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
108 15 Acc = mongoose_acc:new(#{ location => ?LOCATION,
109 lserver => FromJID#jid.lserver,
110 element => IQEl,
111 from_jid => FromJID,
112 to_jid => ToJID }),
113 15 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
114
115 15 {reply, ringing}.
116
117 sip_reinvite_unsafe(Req, _Call) ->
118 4 ?LOG_INFO(#{what => sip_reinvite, sip_req => Req}),
119 4 {FromJID, FromBinary} = get_user_from_sip_msg(from, Req),
120 4 {ToJID, ToBinary} = get_user_from_sip_msg(to, Req),
121
122 4 CallID = nksip_sipmsg:header(<<"call-id">>, Req),
123 4 Body = nksip_sipmsg:meta(body, Req),
124
125 4 {CodecMap, SDP} = nksip_sdp_util:extract_codec_map(Body),
126 4 RemainingAttrs = SDP#sdp.attributes,
127 4 OtherEls = sip_to_jingle:parse_sdp_attributes(RemainingAttrs),
128
129 4 ContentEls = [sip_to_jingle:sdp_media_to_content_el(Media, CodecMap) || Media <- SDP#sdp.medias],
130 4 Name = get_action_name_from_sdp(RemainingAttrs, <<"transport-info">>),
131 4 JingleEl = jingle_sip_helper:jingle_element(CallID, Name, ContentEls ++ OtherEls),
132
133 4 ?LOG_INFO(#{what => sip_reinvite, text => <<"Got SIP re-INVITE from NkSIP">>,
134 from_jid => FromBinary, to_jid => ToBinary,
135 4 call_id => CallID, sip_req => Req}),
136
137 4 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
138 4 Acc = mongoose_acc:new(#{ location => ?LOCATION,
139 lserver => FromJID#jid.lserver,
140 element => IQEl,
141 from_jid => FromJID,
142 to_jid => ToJID }),
143 4 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
144 4 {reply, ok}.
145
146 get_action_name_from_sdp(Attrs, Default) ->
147 4 case lists:keyfind(<<"jingle-action">>, 1, Attrs) of
148 {_, [Name]} ->
149 3 Name;
150 _ ->
151 1 Default
152 end.
153
154
155 sip_info(Req, _Call) ->
156
:-(
?LOG_INFO(#{what => sip_info, sip_req => Req}),
157
:-(
noreply.
158
159 sip_bye(Req, _Call) ->
160 9 {FromJID, FromBinary} = get_user_from_sip_msg(from, Req),
161 9 {ToJID, ToBinary} = get_user_from_sip_msg(to, Req),
162
163 9 CallID = nksip_sipmsg:header(<<"call-id">>, Req),
164 9 ReasonEl = #xmlel{name = <<"reason">>,
165 children = [#xmlel{name = <<"success">>}]},
166 9 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-terminate">>, [ReasonEl]),
167 9 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
168 9 Acc = mongoose_acc:new(#{ location => ?LOCATION,
169 lserver => FromJID#jid.lserver,
170 element => IQEl,
171 from_jid => FromJID,
172 to_jid => ToJID }),
173 9 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
174
175 9 {reply, ok}.
176
177 sip_cancel(_InviteReq, Req, _Call) ->
178 1 {FromJID, FromBinary} = get_user_from_sip_msg(from, Req),
179 1 {ToJID, ToBinary} = get_user_from_sip_msg(to, Req),
180
181 1 CallID = nksip_sipmsg:header(<<"call-id">>, Req),
182 1 ReasonEl = #xmlel{name = <<"reason">>,
183 children = [#xmlel{name = <<"decline">>}]},
184 1 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-terminate">>, [ReasonEl]),
185 1 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
186 1 Acc = mongoose_acc:new(#{ location => ?LOCATION,
187 lserver => FromJID#jid.lserver,
188 element => IQEl,
189 from_jid => FromJID,
190 to_jid => ToJID }),
191 1 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
192
193 1 {reply, ok}.
194
195 sip_dialog_update(start, Dialog, Call) ->
196 32 {ok, DialogHandle} = nksip_dialog:get_handle(Dialog),
197 32 [Transaction | _] = Call#call.trans,
198 32 case Transaction#trans.class of
199 uas ->
200 15 {ok, CallID} = nksip_dialog:call_id(Dialog),
201 15 mod_jingle_sip_backend:set_incoming_handle(CallID, DialogHandle);
202
203 _ ->
204 17 ok
205 end,
206 32 noreply;
207 sip_dialog_update(_, _, _) ->
208 125 noreply.
209
210 %% @doc
211 %% This function is called for every response to the SIP INVITE
212 %% SIP response contains the same headers as request
213 %% That's why we need to switch `from' and `to' when preparing and routing Jingle
214 %% to the request originator
215 %% interpreted status codes:
216 %% * 180 and 183 - provisional respons - we can send `ringing' session-info
217 %% * 200 - the invite was accepted we can sent `session-accepted' stanza
218 %% * 487 - this is to confirm INVITE cancelation from the other side (no action in this case)
219 %% * 603 - used to decline the INVITE by the reciving side
220 %% * all error responses between 400 and 700 result in genering session-terminate reason
221 invite_resp_callback({resp, 180, SIPMsg, _Call}) ->
222 15 send_ringing_session_info(SIPMsg, 180);
223 invite_resp_callback({resp, 183, SIPMsg, _Call}) ->
224 2 send_ringing_session_info(SIPMsg, 183);
225 invite_resp_callback({resp, 200, SIPMsg, _Call}) ->
226 11 {ToJID, ToBinary} = get_user_from_sip_msg(from, SIPMsg),
227 11 {FromJID, FromBinary} = get_user_from_sip_msg(to, SIPMsg),
228
229 11 Body = nksip_sipmsg:meta(body, SIPMsg),
230 11 CallID = nksip_sipmsg:header(<<"call-id">>, SIPMsg),
231 11 {CodecMap, SDP} = nksip_sdp_util:extract_codec_map(Body),
232 11 OtherEls = sip_to_jingle:parse_sdp_attributes(SDP#sdp.attributes),
233
234
235 11 ContentEls = [sip_to_jingle:sdp_media_to_content_el(Media, CodecMap) || Media <- SDP#sdp.medias],
236
237 11 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-accept">>, ContentEls ++ OtherEls),
238 11 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
239 11 Acc = mongoose_acc:new(#{ location => ?LOCATION,
240 lserver => FromJID#jid.lserver,
241 element => IQEl,
242 from_jid => FromJID,
243 to_jid => ToJID }),
244 11 ok = mod_jingle_sip_backend:set_outgoing_accepted(CallID),
245 11 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
246 11 ok;
247 invite_resp_callback({resp, 487, _SIPMsg, _Call}) ->
248 %% this error response only confirms that that the transaction was canceled
249 %% the real `session-terminate` stanza is sent by `sip_cancel/3` callback
250 1 ok;
251 invite_resp_callback({resp, 486, SIPMsg, _Call}) ->
252 2 {ToJID, ToBinary} = get_user_from_sip_msg(from, SIPMsg),
253 2 {FromJID, FromBinary} = get_user_from_sip_msg(to, SIPMsg),
254 2 CallID = nksip_sipmsg:header(<<"call-id">>, SIPMsg),
255
256 2 ReasonEl = #xmlel{name = <<"reason">>,
257 children = [#xmlel{name = <<"decline">>}]},
258 2 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-terminate">>, [ReasonEl]),
259 2 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
260 2 Acc = mongoose_acc:new(#{ location => ?LOCATION,
261 lserver => FromJID#jid.lserver,
262 element => IQEl,
263 from_jid => FromJID,
264 to_jid => ToJID }),
265 2 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
266 2 ok;
267 invite_resp_callback({resp, ErrorCode, SIPMsg, _Call})
268 when ErrorCode >= 400, ErrorCode =< 700 ->
269 4 {ToJID, ToBinary} = get_user_from_sip_msg(from, SIPMsg),
270 4 {FromJID, FromBinary} = get_user_from_sip_msg(to, SIPMsg),
271 4 CallID = nksip_sipmsg:header(<<"call-id">>, SIPMsg),
272
273 4 ReasonEl = make_session_terminate_reason_el(ErrorCode, SIPMsg),
274
275 4 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-terminate">>, [ReasonEl]),
276 4 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
277 4 Acc = mongoose_acc:new(#{ location => ?LOCATION,
278 lserver => FromJID#jid.lserver,
279 element => IQEl,
280 from_jid => FromJID,
281 to_jid => ToJID }),
282 4 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
283 4 ok;
284 invite_resp_callback(Data) ->
285
:-(
?LOG_ERROR(#{what => sip_unknown_response, sip_data => Data}).
286
287 send_ringing_session_info(SIPMsg, ErrorCode) ->
288 %% SIP response contains the same headers as request
289 %% That's why we need to switch `from` and `to` when preparing Jingle packet
290 17 {ToJID, ToBinary} = get_user_from_sip_msg(from, SIPMsg),
291 17 {FromJID, FromBinary} = get_user_from_sip_msg(to, SIPMsg),
292
293 17 DialogHandle = nksip_sipmsg:meta(dialog_handle, SIPMsg),
294 17 {SrvId, DialogId, CallID} = nksip_dialog_lib:parse_handle(DialogHandle),
295 17 ?LOG_INFO(#{what => sip_invite_resp_callback, error_code => ErrorCode,
296 call_id => CallID, sip_req => SIPMsg,
297 dialog_id => DialogId, server_id => SrvId,
298 17 from_jid => FromBinary, to_binary => ToBinary}),
299
300 17 mod_jingle_sip_backend:set_outgoing_handle(CallID, DialogHandle, FromJID, ToJID),
301
302 17 RingingEl = #xmlel{name = <<"ringing">>,
303 attrs = [{<<"xmlns">>, <<"urn:xmpp:jingle:apps:rtp:info:1">>}]},
304 17 JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-info">>, [RingingEl]),
305 17 IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl),
306 17 Acc = mongoose_acc:new(#{ location => ?LOCATION,
307 lserver => FromJID#jid.lserver,
308 element => IQEl,
309 from_jid => FromJID,
310 to_jid => ToJID }),
311 17 maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl),
312 17 ok.
313
314 get_user_from_sip_msg(Field, SIPMsg) ->
315 128 URI = nksip_sipmsg:meta(Field, SIPMsg),
316 128 #uri{user = ToUserIn, domain = ToDomain, path = ToPath} = URI,
317
318 128 Resource = path_to_res(ToPath),
319
320 128 ToUser = jingle_sip_helper:maybe_rewrite_from_phone(ToDomain, ToUserIn),
321
322 128 ToJID = jid:make(ToUser, ToDomain, Resource),
323 128 {ToJID, jid:to_binary({ToUser, ToDomain, Resource})}.
324
325 path_to_res(<<"/", Rest/binary>>) ->
326
:-(
Rest;
327 path_to_res(Other) ->
328 128 Other.
329
330 make_session_terminate_reason_el(ErrorCode, #sipmsg{class = {resp, ErrorCode, Binary}}) ->
331 4 Reason = #xmlel{name = <<"general-error">>},
332 4 Details = #xmlel{name = <<"sip-error">>,
333 attrs = [{<<"code">>, integer_to_binary(ErrorCode)}],
334 children = [#xmlcdata{content = Binary}]},
335 4 #xmlel{name = <<"reason">>,
336 children = [Reason, Details]}.
337
338 maybe_route_to_all_sessions(From, To, Acc, Packet) ->
339 63 PResources = ejabberd_sm:get_user_present_resources(To),
340 63 lists:foreach(
341 fun({_, R}) ->
342 55 ejabberd_router:route(From, jid:replace_resource(To, R), Acc, Packet)
343 end, PResources).
344
Line Hits Source