mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-24 16:23:40 +01:00
578 lines
17 KiB
Erlang
578 lines
17 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% File : mod_sip_registrar.erl
|
|
%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net>
|
|
%%% Purpose :
|
|
%%% Created : 23 Apr 2014 by Evgeny Khramtsov <ekhramtsov@process-one.net>
|
|
%%%
|
|
%%%
|
|
%%% ejabberd, Copyright (C) 2014-2023 ProcessOne
|
|
%%%
|
|
%%% This program is free software; you can redistribute it and/or
|
|
%%% modify it under the terms of the GNU General Public License as
|
|
%%% published by the Free Software Foundation; either version 2 of the
|
|
%%% License, or (at your option) any later version.
|
|
%%%
|
|
%%% This program is distributed in the hope that it will be useful,
|
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
%%% General Public License for more details.
|
|
%%%
|
|
%%% You should have received a copy of the GNU General Public License along
|
|
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
%%%
|
|
%%%-------------------------------------------------------------------
|
|
|
|
-module(mod_sip_registrar).
|
|
|
|
-ifndef(SIP).
|
|
-export([]).
|
|
-else.
|
|
-ifndef(GEN_SERVER).
|
|
-define(GEN_SERVER, gen_server).
|
|
-endif.
|
|
-behaviour(?GEN_SERVER).
|
|
|
|
%% API
|
|
-export([start_link/0, request/2, find_sockets/2, ping/1]).
|
|
|
|
%% gen_server callbacks
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
|
terminate/2, code_change/3]).
|
|
|
|
-include("logger.hrl").
|
|
-include_lib("esip/include/esip.hrl").
|
|
|
|
-define(CALL_TIMEOUT, timer:seconds(30)).
|
|
-define(DEFAULT_EXPIRES, 3600).
|
|
|
|
-record(sip_session, {us = {<<"">>, <<"">>} :: {binary(), binary()},
|
|
socket = #sip_socket{} :: #sip_socket{},
|
|
call_id = <<"">> :: binary(),
|
|
cseq = 0 :: non_neg_integer(),
|
|
timestamp = erlang:timestamp() :: erlang:timestamp(),
|
|
contact :: {binary(), #uri{}, [{binary(), binary()}]},
|
|
flow_tref :: reference() | undefined,
|
|
reg_tref = make_ref() :: reference(),
|
|
conn_mref = make_ref() :: reference(),
|
|
expires = 0 :: non_neg_integer()}).
|
|
|
|
-record(state, {}).
|
|
|
|
%%%===================================================================
|
|
%%% API
|
|
%%%===================================================================
|
|
start_link() ->
|
|
?GEN_SERVER:start_link({local, ?MODULE}, ?MODULE, [], []).
|
|
|
|
request(#sip{hdrs = Hdrs} = Req, SIPSock) ->
|
|
{_, #uri{user = U, host = S}, _} = esip:get_hdr('to', Hdrs),
|
|
LUser = jid:nodeprep(U),
|
|
LServer = jid:nameprep(S),
|
|
{PeerIP, _} = SIPSock#sip_socket.peer,
|
|
US = {LUser, LServer},
|
|
CallID = esip:get_hdr('call-id', Hdrs),
|
|
CSeq = esip:get_hdr('cseq', Hdrs),
|
|
Expires = esip:get_hdr('expires', Hdrs, ?DEFAULT_EXPIRES),
|
|
Supported = esip:get_hdrs('supported', Hdrs),
|
|
IsOutboundSupported = lists:member(<<"outbound">>, Supported),
|
|
case esip:get_hdrs('contact', Hdrs) of
|
|
[<<"*">>] when Expires == 0 ->
|
|
case unregister_session(US, CallID, CSeq) of
|
|
{ok, ContactsWithExpires} ->
|
|
?INFO_MSG("Unregister SIP session for user ~ts@~ts from ~ts",
|
|
[LUser, LServer, inet_parse:ntoa(PeerIP)]),
|
|
Cs = prepare_contacts_to_send(ContactsWithExpires),
|
|
mod_sip:make_response(
|
|
Req,
|
|
#sip{type = response,
|
|
status = 200,
|
|
hdrs = [{'contact', Cs}]});
|
|
{error, Why} ->
|
|
{Status, Reason} = make_status(Why),
|
|
mod_sip:make_response(
|
|
Req, #sip{type = response,
|
|
status = Status,
|
|
reason = Reason})
|
|
end;
|
|
[{_, _URI, _Params}|_] = Contacts ->
|
|
ContactsWithExpires = make_contacts_with_expires(Contacts, Expires),
|
|
ContactsHaveManyRegID = contacts_have_many_reg_id(Contacts),
|
|
Expires1 = lists:max([E || {_, E} <- ContactsWithExpires]),
|
|
MinExpires = min_expires(),
|
|
if Expires1 > 0, Expires1 < MinExpires ->
|
|
mod_sip:make_response(
|
|
Req, #sip{type = response,
|
|
status = 423,
|
|
hdrs = [{'min-expires', MinExpires}]});
|
|
ContactsHaveManyRegID ->
|
|
mod_sip:make_response(
|
|
Req, #sip{type = response, status = 400,
|
|
reason = <<"Multiple 'reg-id' parameter">>});
|
|
true ->
|
|
case register_session(US, SIPSock, CallID, CSeq,
|
|
IsOutboundSupported,
|
|
ContactsWithExpires) of
|
|
{ok, Res} ->
|
|
?INFO_MSG("~ts SIP session for user ~ts@~ts from ~ts",
|
|
[Res, LUser, LServer,
|
|
inet_parse:ntoa(PeerIP)]),
|
|
Cs = prepare_contacts_to_send(ContactsWithExpires),
|
|
Require = case need_ob_hdrs(
|
|
Contacts, IsOutboundSupported) of
|
|
true -> [{'require', [<<"outbound">>]},
|
|
{'flow-timer',
|
|
get_flow_timeout(LServer, SIPSock)}];
|
|
false -> []
|
|
end,
|
|
mod_sip:make_response(
|
|
Req,
|
|
#sip{type = response,
|
|
status = 200,
|
|
hdrs = [{'contact', Cs}|Require]});
|
|
{error, Why} ->
|
|
{Status, Reason} = make_status(Why),
|
|
mod_sip:make_response(
|
|
Req, #sip{type = response,
|
|
status = Status,
|
|
reason = Reason})
|
|
end
|
|
end;
|
|
[] ->
|
|
case mnesia:dirty_read(sip_session, US) of
|
|
[_|_] = Sessions ->
|
|
ContactsWithExpires =
|
|
lists:map(
|
|
fun(#sip_session{contact = Contact, expires = Es}) ->
|
|
{Contact, Es}
|
|
end, Sessions),
|
|
Cs = prepare_contacts_to_send(ContactsWithExpires),
|
|
mod_sip:make_response(
|
|
Req, #sip{type = response, status = 200,
|
|
hdrs = [{'contact', Cs}]});
|
|
[] ->
|
|
{Status, Reason} = make_status(notfound),
|
|
mod_sip:make_response(
|
|
Req, #sip{type = response,
|
|
status = Status,
|
|
reason = Reason})
|
|
end;
|
|
_ ->
|
|
mod_sip:make_response(Req, #sip{type = response, status = 400})
|
|
end.
|
|
|
|
find_sockets(U, S) ->
|
|
case mnesia:dirty_read(sip_session, {U, S}) of
|
|
[_|_] = Sessions ->
|
|
lists:map(
|
|
fun(#sip_session{contact = {_, URI, _},
|
|
socket = Socket}) ->
|
|
{Socket, URI}
|
|
end, Sessions);
|
|
[] ->
|
|
[]
|
|
end.
|
|
|
|
ping(SIPSocket) ->
|
|
call({ping, SIPSocket}).
|
|
|
|
%%%===================================================================
|
|
%%% gen_server callbacks
|
|
%%%===================================================================
|
|
init([]) ->
|
|
process_flag(trap_exit, true),
|
|
update_table(),
|
|
ejabberd_mnesia:create(?MODULE, sip_session,
|
|
[{ram_copies, [node()]},
|
|
{type, bag},
|
|
{attributes, record_info(fields, sip_session)},
|
|
{index, [conn_mref,socket]}]),
|
|
{ok, #state{}}.
|
|
|
|
handle_call({write, Sessions, Supported}, _From, State) ->
|
|
Res = write_session(Sessions, Supported),
|
|
{reply, Res, State};
|
|
handle_call({delete, US, CallID, CSeq}, _From, State) ->
|
|
Res = delete_session(US, CallID, CSeq),
|
|
{reply, Res, State};
|
|
handle_call({ping, SIPSocket}, _From, State) ->
|
|
Res = process_ping(SIPSocket),
|
|
{reply, Res, State};
|
|
handle_call(Request, From, State) ->
|
|
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
|
|
{noreply, State}.
|
|
|
|
handle_cast(Msg, State) ->
|
|
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
|
|
{noreply, State}.
|
|
|
|
handle_info({write, Sessions, Supported}, State) ->
|
|
write_session(Sessions, Supported),
|
|
{noreply, State};
|
|
handle_info({delete, US, CallID, CSeq}, State) ->
|
|
delete_session(US, CallID, CSeq),
|
|
{noreply, State};
|
|
handle_info({timeout, TRef, US}, State) ->
|
|
delete_expired_session(US, TRef),
|
|
{noreply, State};
|
|
handle_info({'DOWN', MRef, process, _Pid, _Reason}, State) ->
|
|
case mnesia:dirty_index_read(sip_session, MRef, #sip_session.conn_mref) of
|
|
[Session] ->
|
|
mnesia:dirty_delete_object(Session);
|
|
_ ->
|
|
ok
|
|
end,
|
|
{noreply, State};
|
|
handle_info(Info, State) ->
|
|
?WARNING_MSG("Unexpected info: ~p", [Info]),
|
|
{noreply, State}.
|
|
|
|
terminate(_Reason, _State) ->
|
|
ok.
|
|
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
{ok, State}.
|
|
|
|
%%%===================================================================
|
|
%%% Internal functions
|
|
%%%===================================================================
|
|
register_session(US, SIPSocket, CallID, CSeq, IsOutboundSupported,
|
|
ContactsWithExpires) ->
|
|
Sessions = lists:map(
|
|
fun({Contact, Expires}) ->
|
|
#sip_session{us = US,
|
|
socket = SIPSocket,
|
|
call_id = CallID,
|
|
cseq = CSeq,
|
|
timestamp = erlang:timestamp(),
|
|
contact = Contact,
|
|
expires = Expires}
|
|
end, ContactsWithExpires),
|
|
Msg = {write, Sessions, IsOutboundSupported},
|
|
call(Msg).
|
|
|
|
unregister_session(US, CallID, CSeq) ->
|
|
Msg = {delete, US, CallID, CSeq},
|
|
call(Msg).
|
|
|
|
write_session([#sip_session{us = {U, S} = US}|_] = NewSessions,
|
|
IsOutboundSupported) ->
|
|
PrevSessions = mnesia:dirty_read(sip_session, US),
|
|
Res = lists:foldl(
|
|
fun(_, {error, _} = Err) ->
|
|
Err;
|
|
(#sip_session{call_id = CallID,
|
|
expires = Expires,
|
|
cseq = CSeq} = Session, {Add, Del}) ->
|
|
case find_session(Session, PrevSessions,
|
|
IsOutboundSupported) of
|
|
{ok, normal, #sip_session{call_id = CallID,
|
|
cseq = PrevCSeq}}
|
|
when PrevCSeq > CSeq ->
|
|
{error, cseq_out_of_order};
|
|
{ok, _Type, PrevSession} when Expires == 0 ->
|
|
{Add, [PrevSession|Del]};
|
|
{ok, _Type, PrevSession} ->
|
|
{[Session|Add], [PrevSession|Del]};
|
|
{error, notfound} when Expires == 0 ->
|
|
{error, notfound};
|
|
{error, notfound} ->
|
|
{[Session|Add], Del}
|
|
end
|
|
end, {[], []}, NewSessions),
|
|
MaxSessions = ejabberd_sm:get_max_user_sessions(U, S),
|
|
case Res of
|
|
{error, Why} ->
|
|
{error, Why};
|
|
{AddSessions, DelSessions} ->
|
|
MaxSessions = ejabberd_sm:get_max_user_sessions(U, S),
|
|
AllSessions = AddSessions ++ PrevSessions -- DelSessions,
|
|
if length(AllSessions) > MaxSessions ->
|
|
{error, too_many_sessions};
|
|
true ->
|
|
lists:foreach(fun delete_session/1, DelSessions),
|
|
lists:foreach(
|
|
fun(Session) ->
|
|
NewSession = set_monitor_and_timer(
|
|
Session, IsOutboundSupported),
|
|
mnesia:dirty_write(NewSession)
|
|
end, AddSessions),
|
|
case {AllSessions, AddSessions} of
|
|
{[], _} ->
|
|
{ok, unregister};
|
|
{_, []} ->
|
|
{ok, unregister};
|
|
_ ->
|
|
{ok, register}
|
|
end
|
|
end
|
|
end.
|
|
|
|
delete_session(US, CallID, CSeq) ->
|
|
case mnesia:dirty_read(sip_session, US) of
|
|
[_|_] = Sessions ->
|
|
case lists:all(
|
|
fun(S) when S#sip_session.call_id == CallID,
|
|
S#sip_session.cseq > CSeq ->
|
|
false;
|
|
(_) ->
|
|
true
|
|
end, Sessions) of
|
|
true ->
|
|
ContactsWithExpires =
|
|
lists:map(
|
|
fun(#sip_session{contact = Contact} = Session) ->
|
|
delete_session(Session),
|
|
{Contact, 0}
|
|
end, Sessions),
|
|
{ok, ContactsWithExpires};
|
|
false ->
|
|
{error, cseq_out_of_order}
|
|
end;
|
|
[] ->
|
|
{error, notfound}
|
|
end.
|
|
|
|
delete_expired_session(US, TRef) ->
|
|
case mnesia:dirty_read(sip_session, US) of
|
|
[_|_] = Sessions ->
|
|
lists:foreach(
|
|
fun(#sip_session{reg_tref = T1,
|
|
flow_tref = T2} = Session)
|
|
when T1 == TRef; T2 == TRef ->
|
|
if T2 /= undefined ->
|
|
close_socket(Session);
|
|
true ->
|
|
ok
|
|
end,
|
|
delete_session(Session);
|
|
(_) ->
|
|
ok
|
|
end, Sessions);
|
|
[] ->
|
|
ok
|
|
end.
|
|
|
|
min_expires() ->
|
|
60.
|
|
|
|
to_integer(Bin, Min, Max) ->
|
|
case catch (binary_to_integer(Bin)) of
|
|
N when N >= Min, N =< Max ->
|
|
{ok, N};
|
|
_ ->
|
|
error
|
|
end.
|
|
|
|
call(Msg) ->
|
|
case catch ?GEN_SERVER:call(?MODULE, Msg, ?CALL_TIMEOUT) of
|
|
{'EXIT', {timeout, _}} ->
|
|
{error, timeout};
|
|
{'EXIT', Why} ->
|
|
{error, Why};
|
|
Reply ->
|
|
Reply
|
|
end.
|
|
|
|
make_contacts_with_expires(Contacts, Expires) ->
|
|
lists:map(
|
|
fun({Name, URI, Params}) ->
|
|
E1 = case to_integer(esip:get_param(<<"expires">>, Params),
|
|
0, (1 bsl 32)-1) of
|
|
{ok, E} -> E;
|
|
_ -> Expires
|
|
end,
|
|
Params1 = lists:keydelete(<<"expires">>, 1, Params),
|
|
{{Name, URI, Params1}, E1}
|
|
end, Contacts).
|
|
|
|
prepare_contacts_to_send(ContactsWithExpires) ->
|
|
lists:map(
|
|
fun({{Name, URI, Params}, Expires}) ->
|
|
Params1 = esip:set_param(<<"expires">>,
|
|
list_to_binary(
|
|
integer_to_list(Expires)),
|
|
Params),
|
|
{Name, URI, Params1}
|
|
end, ContactsWithExpires).
|
|
|
|
contacts_have_many_reg_id(Contacts) ->
|
|
Sum = lists:foldl(
|
|
fun({_Name, _URI, Params}, Acc) ->
|
|
case get_ob_params(Params) of
|
|
error ->
|
|
Acc;
|
|
{_, _} ->
|
|
Acc + 1
|
|
end
|
|
end, 0, Contacts),
|
|
if Sum > 1 ->
|
|
true;
|
|
true ->
|
|
false
|
|
end.
|
|
|
|
find_session(#sip_session{contact = {_, URI, Params}}, Sessions,
|
|
IsOutboundSupported) ->
|
|
if IsOutboundSupported ->
|
|
case get_ob_params(Params) of
|
|
{InstanceID, RegID} ->
|
|
find_session_by_ob({InstanceID, RegID}, Sessions);
|
|
error ->
|
|
find_session_by_uri(URI, Sessions)
|
|
end;
|
|
true ->
|
|
find_session_by_uri(URI, Sessions)
|
|
end.
|
|
|
|
find_session_by_ob({InstanceID, RegID},
|
|
[#sip_session{contact = {_, _, Params}} = Session|Sessions]) ->
|
|
case get_ob_params(Params) of
|
|
{InstanceID, RegID} ->
|
|
{ok, flow, Session};
|
|
_ ->
|
|
find_session_by_ob({InstanceID, RegID}, Sessions)
|
|
end;
|
|
find_session_by_ob(_, []) ->
|
|
{error, notfound}.
|
|
|
|
find_session_by_uri(URI1,
|
|
[#sip_session{contact = {_, URI2, _}} = Session|Sessions]) ->
|
|
case cmp_uri(URI1, URI2) of
|
|
true ->
|
|
{ok, normal, Session};
|
|
false ->
|
|
find_session_by_uri(URI1, Sessions)
|
|
end;
|
|
find_session_by_uri(_, []) ->
|
|
{error, notfound}.
|
|
|
|
%% TODO: this is *totally* wrong.
|
|
%% Rewrite this using URI comparison rules
|
|
cmp_uri(#uri{user = U, host = H, port = P},
|
|
#uri{user = U, host = H, port = P}) ->
|
|
true;
|
|
cmp_uri(_, _) ->
|
|
false.
|
|
|
|
make_status(notfound) ->
|
|
{404, esip:reason(404)};
|
|
make_status(cseq_out_of_order) ->
|
|
{500, <<"CSeq is Out of Order">>};
|
|
make_status(timeout) ->
|
|
{408, esip:reason(408)};
|
|
make_status(too_many_sessions) ->
|
|
{503, <<"Too Many Registered Sessions">>};
|
|
make_status(_) ->
|
|
{500, esip:reason(500)}.
|
|
|
|
get_ob_params(Params) ->
|
|
case esip:get_param(<<"+sip.instance">>, Params) of
|
|
<<>> ->
|
|
error;
|
|
InstanceID ->
|
|
case to_integer(esip:get_param(<<"reg-id">>, Params),
|
|
0, (1 bsl 32)-1) of
|
|
{ok, RegID} ->
|
|
{InstanceID, RegID};
|
|
error ->
|
|
error
|
|
end
|
|
end.
|
|
|
|
need_ob_hdrs(_Contacts, _IsOutboundSupported = false) ->
|
|
false;
|
|
need_ob_hdrs(Contacts, _IsOutboundSupported = true) ->
|
|
lists:any(
|
|
fun({_Name, _URI, Params}) ->
|
|
case get_ob_params(Params) of
|
|
error -> false;
|
|
{_, _} -> true
|
|
end
|
|
end, Contacts).
|
|
|
|
get_flow_timeout(LServer, #sip_socket{type = Type}) ->
|
|
case Type of
|
|
udp ->
|
|
mod_sip_opt:flow_timeout_udp(LServer) div 1000;
|
|
_ ->
|
|
mod_sip_opt:flow_timeout_tcp(LServer) div 1000
|
|
end.
|
|
|
|
update_table() ->
|
|
Fields = record_info(fields, sip_session),
|
|
case catch mnesia:table_info(sip_session, attributes) of
|
|
Fields ->
|
|
ok;
|
|
[_|_] ->
|
|
mnesia:delete_table(sip_session);
|
|
{'EXIT', _} ->
|
|
ok
|
|
end.
|
|
|
|
set_monitor_and_timer(#sip_session{socket = #sip_socket{type = Type,
|
|
pid = Pid} = SIPSock,
|
|
conn_mref = MRef,
|
|
expires = Expires,
|
|
us = {_, LServer},
|
|
contact = {_, _, Params}} = Session,
|
|
IsOutboundSupported) ->
|
|
RegTRef = set_timer(Session, Expires),
|
|
Session1 = Session#sip_session{reg_tref = RegTRef},
|
|
if IsOutboundSupported ->
|
|
case get_ob_params(Params) of
|
|
error ->
|
|
Session1;
|
|
{_, _} ->
|
|
FlowTimeout = get_flow_timeout(LServer, SIPSock),
|
|
FlowTRef = set_timer(Session1, FlowTimeout),
|
|
NewMRef = if Type == udp -> MRef;
|
|
true -> erlang:monitor(process, Pid)
|
|
end,
|
|
Session1#sip_session{conn_mref = NewMRef,
|
|
flow_tref = FlowTRef}
|
|
end;
|
|
true ->
|
|
Session1
|
|
end.
|
|
|
|
set_timer(#sip_session{us = US}, Timeout) ->
|
|
erlang:start_timer(Timeout * 1000, self(), US).
|
|
|
|
close_socket(#sip_session{socket = SIPSocket}) ->
|
|
if SIPSocket#sip_socket.type /= udp ->
|
|
esip_socket:close(SIPSocket);
|
|
true ->
|
|
ok
|
|
end.
|
|
|
|
delete_session(#sip_session{reg_tref = RegTRef,
|
|
flow_tref = FlowTRef,
|
|
conn_mref = MRef} = Session) ->
|
|
misc:cancel_timer(RegTRef),
|
|
misc:cancel_timer(FlowTRef),
|
|
catch erlang:demonitor(MRef, [flush]),
|
|
mnesia:dirty_delete_object(Session).
|
|
|
|
process_ping(SIPSocket) ->
|
|
ErrResponse = if SIPSocket#sip_socket.type == udp -> pang;
|
|
true -> drop
|
|
end,
|
|
Sessions = mnesia:dirty_index_read(
|
|
sip_session, SIPSocket, #sip_session.socket),
|
|
lists:foldl(
|
|
fun(#sip_session{flow_tref = TRef,
|
|
us = {_, LServer}} = Session, _)
|
|
when TRef /= undefined ->
|
|
erlang:cancel_timer(TRef),
|
|
mnesia:dirty_delete_object(Session),
|
|
Timeout = get_flow_timeout(LServer, SIPSocket),
|
|
NewTRef = set_timer(Session, Timeout),
|
|
mnesia:dirty_write(Session#sip_session{flow_tref = NewTRef}),
|
|
pong;
|
|
(_, Acc) ->
|
|
Acc
|
|
end, ErrResponse, Sessions).
|
|
|
|
-endif.
|