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 |
9 |
{FromJID, FromBinary} = get_user_from_sip_msg(from, Req), |
166 |
9 |
{ToJID, ToBinary} = get_user_from_sip_msg(to, Req), |
167 |
|
|
168 |
9 |
CallID = nksip_sipmsg:header(<<"call-id">>, Req), |
169 |
9 |
ReasonEl = #xmlel{name = <<"reason">>, |
170 |
|
children = [#xmlel{name = <<"success">>}]}, |
171 |
9 |
JingleEl = jingle_sip_helper:jingle_element(CallID, <<"session-terminate">>, [ReasonEl]), |
172 |
9 |
IQEl = jingle_sip_helper:jingle_iq(ToBinary, FromBinary, JingleEl), |
173 |
9 |
Acc = mongoose_acc:new(#{ location => ?LOCATION, |
174 |
|
lserver => FromJID#jid.lserver, |
175 |
|
element => IQEl, |
176 |
|
from_jid => FromJID, |
177 |
|
to_jid => ToJID }), |
178 |
9 |
maybe_route_to_all_sessions(FromJID, ToJID, Acc, IQEl), |
179 |
9 |
ok = mod_jingle_sip_session:remove_session(CallID), |
180 |
9 |
{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 |
125 |
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 |
128 |
URI = nksip_sipmsg:meta(Field, SIPMsg), |
323 |
128 |
#uri{user = ToUserIn, domain = ToDomain, path = ToPath} = URI, |
324 |
|
|
325 |
128 |
Resource = path_to_res(ToPath), |
326 |
|
|
327 |
128 |
ToUser = jingle_sip_helper:maybe_rewrite_from_phone(ToDomain, ToUserIn), |
328 |
|
|
329 |
128 |
ToJID = jid:make(ToUser, ToDomain, Resource), |
330 |
128 |
{ToJID, jid:to_binary({ToUser, ToDomain, Resource})}. |
331 |
|
|
332 |
|
path_to_res(<<"/", Rest/binary>>) -> |
333 |
:-( |
Rest; |
334 |
|
path_to_res(Other) -> |
335 |
128 |
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 |
63 |
PResources = ejabberd_sm:get_user_present_resources(To), |
347 |
63 |
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}). |