diff --git a/configure b/configure index 4114b2e69..e26d6e894 100755 --- a/configure +++ b/configure @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.67 for ejabberd community. +# Generated by GNU Autoconf 2.67 for ejabberd community 13.12-100-gec6c58a. # # Report bugs to . # @@ -552,8 +552,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='ejabberd' PACKAGE_TARNAME='ejabberd' -PACKAGE_VERSION='community' -PACKAGE_STRING='ejabberd community' +PACKAGE_VERSION='community 13.12-100-gec6c58a' +PACKAGE_STRING='ejabberd community 13.12-100-gec6c58a' PACKAGE_BUGREPORT='ejabberd@process-one.net' PACKAGE_URL='' @@ -561,6 +561,7 @@ ac_default_prefix=/ ac_subst_vars='LTLIBOBJS LIBOBJS tools +sip lager http debug @@ -672,6 +673,7 @@ enable_iconv enable_debug enable_http enable_lager +enable_sip enable_user ' ac_precious_vars='build_alias @@ -1222,7 +1224,7 @@ if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures ejabberd community to adapt to many kinds of systems. +\`configure' configures ejabberd community 13.12-100-gec6c58a to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1283,7 +1285,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of ejabberd community:";; + short | recursive ) echo "Configuration of ejabberd community 13.12-100-gec6c58a:";; esac cat <<\_ACEOF @@ -1309,8 +1311,8 @@ Optional Features: --enable-pgsql --enable-pam --enable-zlib --enable-stun --enable-riak --enable-json --enable-iconv --enable-debug --enable-http - --enable-lager --enable-tools (useful for Dialyzer - checks, default: no) + --enable-lager --enable-sip --enable-tools (useful + for Dialyzer checks, default: no) --enable-tools build development tools (default: no) --enable-nif replace some functions with C equivalents. Requires Erlang R13B04 or higher (default: no) @@ -1327,6 +1329,7 @@ Optional Features: --enable-http build external HTTP libraries ('ibrowse' and 'lhttpc', default: no) --enable-lager enable lager support (default: yes) + --enable-sip enable SIP support (default: no) --enable-user[[[=USER]]] allow this system user to start ejabberd (default: no) @@ -1407,7 +1410,7 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -ejabberd configure community +ejabberd configure community 13.12-100-gec6c58a generated by GNU Autoconf 2.67 Copyright (C) 2010 Free Software Foundation, Inc. @@ -1466,7 +1469,7 @@ cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by ejabberd $as_me community, which was +It was created by ejabberd $as_me community 13.12-100-gec6c58a, which was generated by GNU Autoconf 2.67. Invocation command line was $ $0 $@ @@ -2480,7 +2483,7 @@ if test "${enable_erlang_version_check+set}" = set; then : enableval=$enable_erlang_version_check; fi - case "$enable_erlang_version_check" in +case "$enable_erlang_version_check" in yes|'') { $as_echo "$as_me:${as_lineno-$LINENO}: checking Erlang/OTP version" >&5 $as_echo_n "checking Erlang/OTP version... " >&6; } @@ -2526,6 +2529,10 @@ parse(Version) -> less_or_equal([], []) -> true; +less_or_equal([], _Any) -> + true; +less_or_equal(_Any, []) -> + false; less_or_equal([Left| Rl], [Right| Rr]) -> case {Left < Right, Left == Right} of {true, _} -> @@ -2608,6 +2615,10 @@ parse(Version) -> less_or_equal([], []) -> true; +less_or_equal([], _Any) -> + true; +less_or_equal(_Any, []) -> + false; less_or_equal([Left| Rl], [Right| Rr]) -> case {Left < Right, Left == Right} of {true, _} -> @@ -2858,8 +2869,8 @@ fi # Check whether --enable-all was given. if test "${enable_all+set}" = set; then : enableval=$enable_all; case "${enableval}" in - yes) nif=true odbc=true mysql=true pgsql=true pam=true zlib=true stun=true riak=true json=true iconv=true debug=true http=true lager=true tools=true ;; - no) nif=false odbc=false mysql=false pgsql=false pam=false zlib=false stun=false riak=false json=false iconv=false debug=false http=false lager=false tools=false ;; + yes) nif=true odbc=true mysql=true pgsql=true pam=true zlib=true stun=true riak=true json=true iconv=true debug=true http=true lager=true sip=true tools=true ;; + no) nif=false odbc=false mysql=false pgsql=false pam=false zlib=false stun=false riak=false json=false iconv=false debug=false http=false lager=false sip=false tools=false ;; *) as_fn_error $? "bad value ${enableval} for --enable-all" "$LINENO" 5 ;; esac fi @@ -3021,6 +3032,18 @@ else fi +# Check whether --enable-sip was given. +if test "${enable_sip+set}" = set; then : + enableval=$enable_sip; case "${enableval}" in + yes) sip=true ;; + no) sip=false ;; + *) as_fn_error $? "bad value ${enableval} for --enable-sip" "$LINENO" 5 ;; +esac +else + if test "x$sip" = "x"; then sip=false; fi +fi + + ac_config_files="$ac_config_files Makefile vars.config src/ejabberd.app.src" @@ -3853,6 +3876,7 @@ fi + cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure @@ -4396,7 +4420,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by ejabberd $as_me community, which was +This file was extended by ejabberd $as_me community 13.12-100-gec6c58a, which was generated by GNU Autoconf 2.67. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -4449,7 +4473,7 @@ _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`" ac_cs_version="\\ -ejabberd config.status community +ejabberd config.status community 13.12-100-gec6c58a configured by $0, generated by GNU Autoconf 2.67, with options \\"\$ac_cs_config\\" diff --git a/configure.ac b/configure.ac index 853d91ecb..6cb0580f9 100644 --- a/configure.ac +++ b/configure.ac @@ -106,10 +106,10 @@ AC_ARG_ENABLE(mssql, esac],[db_type=generic]) AC_ARG_ENABLE(all, -[AC_HELP_STRING([--enable-all], [same as --enable-nif --enable-odbc --enable-mysql --enable-pgsql --enable-pam --enable-zlib --enable-stun --enable-riak --enable-json --enable-iconv --enable-debug --enable-http --enable-lager --enable-tools (useful for Dialyzer checks, default: no)])], +[AC_HELP_STRING([--enable-all], [same as --enable-nif --enable-odbc --enable-mysql --enable-pgsql --enable-pam --enable-zlib --enable-stun --enable-riak --enable-json --enable-iconv --enable-debug --enable-http --enable-lager --enable-sip --enable-tools (useful for Dialyzer checks, default: no)])], [case "${enableval}" in - yes) nif=true odbc=true mysql=true pgsql=true pam=true zlib=true stun=true riak=true json=true iconv=true debug=true http=true lager=true tools=true ;; - no) nif=false odbc=false mysql=false pgsql=false pam=false zlib=false stun=false riak=false json=false iconv=false debug=false http=false lager=false tools=false ;; + yes) nif=true odbc=true mysql=true pgsql=true pam=true zlib=true stun=true riak=true json=true iconv=true debug=true http=true lager=true sip=true tools=true ;; + no) nif=false odbc=false mysql=false pgsql=false pam=false zlib=false stun=false riak=false json=false iconv=false debug=false http=false lager=false sip=false tools=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-all) ;; esac],[]) @@ -217,6 +217,14 @@ AC_ARG_ENABLE(lager, *) AC_MSG_ERROR(bad value ${enableval} for --enable-lager) ;; esac],[if test "x$lager" = "x"; then lager=true; fi]) +AC_ARG_ENABLE(sip, +[AC_HELP_STRING([--enable-sip], [enable SIP support (default: no)])], +[case "${enableval}" in + yes) sip=true ;; + no) sip=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-sip) ;; +esac],[if test "x$sip" = "x"; then sip=false; fi]) + AC_CONFIG_FILES([Makefile vars.config src/ejabberd.app.src]) @@ -277,6 +285,7 @@ AC_SUBST(iconv) AC_SUBST(debug) AC_SUBST(http) AC_SUBST(lager) +AC_SUBST(sip) AC_SUBST(tools) AC_OUTPUT diff --git a/doc/guide.tex b/doc/guide.tex index 21e66a084..488ab6a3d 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -93,6 +93,7 @@ \newcommand{\modsharedroster}{\module{mod\_shared\_roster}} \newcommand{\modsharedrosterldap}{\module{mod\_shared\_roster\_ldap}} \newcommand{\modsic}{\module{mod\_sic}} +\newcommand{\modsip}{\module{mod\_sip}} \newcommand{\modstats}{\module{mod\_stats}} \newcommand{\modtime}{\module{mod\_time}} \newcommand{\modvcard}{\module{mod\_vcard}} @@ -396,6 +397,9 @@ Some options that you may be interested in modifying: \titem{--enable-zlib} Enable Stream Compression (XEP-0138) using zlib. + \titem{--enable-sip} + Enable SIP support (see section \ref{sip}). + \titem{--enable-stun} Enable STUN support (see section \ref{stun}). @@ -883,6 +887,10 @@ The available modules, their purpose and the options allowed by each one are: (as defined in the Jabber Component Protocol (\xepref{0114}).\\ Options: \texttt{access}, \texttt{hosts}, \texttt{max\_fsm\_queue}, \texttt{service\_check\_from}, \texttt{shaper\_rule} + \titem{\texttt{ejabberd\_sip}} + Handles SIP requests as defined in + \footahref{http://tools.ietf.org/html/rfc3261}{RFC 3261}.\\ + Options: \texttt{certfile}, \texttt{tls} \titem{\texttt{ejabberd\_stun}} Handles STUN Binding requests as defined in \footahref{http://tools.ietf.org/html/rfc5389}{RFC 5389}.\\ @@ -1961,7 +1969,7 @@ listen: \ejabberd{} is able to act as a stand-alone STUN server (\footahref{http://tools.ietf.org/html/rfc5389}{RFC 5389}). Currently only Binding usage -is supported. In that role \ejabberd{} helps clients with Jingle ICE (\xepref{0176}) support to discover their external addresses and ports. +is supported. In that role \ejabberd{} helps clients with ICE (\footahref{http://tools.ietf.org/html/rfc5245}{RFC 5245}) or Jingle ICE (\xepref{0176}) support to discover their external addresses and ports. You should configure \term{ejabberd\_stun} listening module as described in \ref{listened} section. If \option{certfile} option is defined, \ejabberd{} multiplexes TCP and @@ -2001,6 +2009,61 @@ _stun._tcp IN SRV 0 0 3478 stun.example.com. _stuns._tcp IN SRV 0 0 5349 stun.example.com. \end{verbatim} +\makesubsection{sip}{SIP} +\ind{options!sip}\ind{sip} + +\ejabberd{} has built-in SIP support. In order to activate it you need to add +listeners for it, configure DNS properly and enable \modsip{} for +the desired virtual host. + +To add a listener you should configure \term{ejabberd\_sip} listening module as +described in \ref{listened} section. If option \option{tls} is specified, option +\option{certfile} must be specified as well, otherwise incoming TLS connections would fail. + +Example configuration with standard ports +(as per \footahref{http://tools.ietf.org/html/rfc3261}{RFC 3261}): +\begin{verbatim} +listen: + ... + - + port: 5060 + transport: udp + module: ejabberd_sip + - + port: 5060 + module: ejabberd_sip + - + port: 5061 + module: ejabberd_sip + tls: true + certfile: "/etc/ejabberd/server.pem" + ... +\end{verbatim} + +Note that there is no StartTLS support in SIP and \footahref{http://en.wikipedia.org/wiki/Server\_Name\_Indication}{SNI} support is somewhat tricky, so for TLS you have to configure +different virtual hosts on different ports if you have different certificate files for them. + +Next you need to configure DNS SIP records for your virtual domains. +Refer to \footahref{http://tools.ietf.org/html/rfc3263}{RFC 3263} for the detailed explanation. +Simply put, you should add NAPTR and SRV records for your domains. +Skip NAPTR configuration if your DNS provider doesn't support this type of records. +It's not fatal, however, highly recommended. + +Example configuration of NAPTR records: +\begin{verbatim} +example.com IN NAPTR 10 0 "s" "SIPS+D2T" "" _sips._tcp.example.com. +example.com IN NAPTR 20 0 "s" "SIP+D2T" "" _sip._tcp.example.com. +example.com IN NAPTR 30 0 "s" "SIP+D2U" "" _sip._udp.example.com. +\end{verbatim} + +Example configuration of SRV records with standard ports +(as per \footahref{http://tools.ietf.org/html/rfc3261}{RFC 3261}): +\begin{verbatim} +_sip._udp IN SRV 0 0 5060 sip.example.com. +_sip._tcp IN SRV 0 0 5060 sip.example.com. +_sips._tcp IN SRV 0 0 5061 sip.example.com. +\end{verbatim} + \makesubsection{includeconfigfile}{Include Additional Configuration Files} \ind{options!includeconfigfile}\ind{includeconfigfile} @@ -2578,6 +2641,7 @@ The following table lists all modules included in \ejabberd{}. \hline \ahrefloc{modsharedroster}{\modsharedroster{}} & Shared roster management & \modroster{} \\ \hline \ahrefloc{modsharedrosterldap}{\modsharedrosterldap{}} & LDAP Shared roster management & \modroster{} \\ \hline \ahrefloc{modsic}{\modsic{}} & Server IP Check (\xepref{0279}) & \\ + \hline \ahrefloc{modsip}{\modsip{}} & SIP Registrar/Proxy (\footahref{http://tools.ietf.org/html/rfc3261}{RFC 3261}) & \term{ejabberd\_sip} \\ \hline \ahrefloc{modstats}{\modstats{}} & Statistics Gathering (\xepref{0039}) & \\ \hline \ahrefloc{modtime}{\modtime{}} & Entity Time (\xepref{0202}) & \\ \hline \ahrefloc{modvcard}{\modvcard{}} & vcard-temp (\xepref{0054}) & \\ @@ -4618,6 +4682,49 @@ Options: \iqdiscitem{\ns{urn:xmpp:sic:0}} \end{description} +\makesubsection{modsip}{\modsip{}} +\ind{modules!\modsip{}} +This module adds SIP proxy/registrar support for the corresponding virtual host. +Note that it is not enough to just load this module only. You should also configure +listeners and DNS records properly. See section \ref{sip} for the full explanation. + +Example configuration: +\begin{verbatim} +modules: + ... + mod_sip: {} + ... +\end{verbatim} + +Options: +\begin{description} +\titem{via: [\{type: Type, host: Host, port: Port\}]}\ind{options!via}With +this option for every \term{Type} you can specify \term{Host} and \term{Port} +to set in \term{Via} header of outgoing SIP messages, where \term{Type} can be +\term{udp}, \term{tcp} or \term{tls}. \term{Host} is a string and \term{Port} is +a non negative integer. This is useful if you're running your server in a non-standard +network topology. Example configuration: +\begin{verbatim} +modules: + ... + mod_sip: + via: + - + type: tls + host: "sip-tls.example.com" + port: 5061 + - + type: tcp + host: "sip-tcp.example.com" + port: 5060 + - + type: udp + host: "sip-udp.example.com" + port: 5060 + ... +\end{verbatim} +\end{description} + \makesubsection{modstats}{\modstats{}} \ind{modules!\modstats{}}\ind{protocols!XEP-0039: Statistics Gathering}\ind{statistics} diff --git a/doc/introduction.tex b/doc/introduction.tex index 163312b38..fee27048c 100644 --- a/doc/introduction.tex +++ b/doc/introduction.tex @@ -128,6 +128,7 @@ Moreover, \ejabberd{} comes with a wide range of other state-of-the-art features \item \txepref{0060}{Publish-Subscribe} component with support for \txepref{0163}{Personal Eventing via Pubsub}. \item Support for web clients: \txepref{0025}{HTTP Polling} and \txepref{0206}{HTTP Binding (BOSH)} services. \item IRC transport. +\item SIP support. \item Component support: interface with networks such as AIM, ICQ and MSN installing special tranports. \end{itemize} \end{itemize} diff --git a/rebar.config.script b/rebar.config.script index 7a5e332c4..dc68d1d30 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -43,7 +43,13 @@ HiPE = case lists:keysearch(hipe, 1, Cfg) of end, Includes = [{i, "include"}, - {i, filename:join(["deps", "p1_xml", "include"])}], + {i, filename:join(["deps", "p1_xml", "include"])}| + lists:flatmap( + fun({sip, true}) -> + [{i, filename:join(["deps", "esip", "include"])}]; + (_) -> + [] + end, Cfg)], SrcDirs = lists:foldl( fun({tools, true}, Acc) -> @@ -97,6 +103,8 @@ CfgDeps = lists:flatmap( ({http, true}) -> [{ibrowse, ".*", {git, "git://github.com/cmullaparthi/ibrowse"}}, {lhttpc, ".*", {git, "git://github.com/esl/lhttpc"}}]; + ({sip, true}) -> + [{esip, ".*", {git, "git://github.com/processone/p1_sip"}}]; ({lager, true}) -> [{lager, ".*", {git, "git://github.com/basho/lager"}}]; ({lager, false}) -> @@ -112,6 +120,8 @@ CfgPostHooks = lists:flatmap( [ConfigureCmd("p1_zlib", "")]; ({iconv, true}) -> [ConfigureCmd("p1_iconv", "")]; + ({sip, true}) -> + [ConfigureCmd("esip", "")]; (_) -> [] end, Cfg), diff --git a/src/ejabberd_listener.erl b/src/ejabberd_listener.erl index 2051afdb2..71f744077 100644 --- a/src/ejabberd_listener.erl +++ b/src/ejabberd_listener.erl @@ -151,6 +151,19 @@ init_udp(PortIP, Module, Opts, SockOpts, Port, IPS) -> {ok, Socket} -> %% Inform my parent that this port was opened succesfully proc_lib:init_ack({ok, self()}), + case erlang:function_exported(Module, udp_init, 2) of + true -> + case catch Module:udp_init(Socket, Opts) of + {'EXIT', _} = Err -> + ?ERROR_MSG("failed to process callback function " + "~p:~s(~p, ~p): ~p", + [Module, udp_init, Socket, Opts, Err]); + _ -> + ok + end; + false -> + ok + end, udp_recv(Socket, Module, Opts); {error, Reason} -> socket_error(Reason, PortIP, Module, SockOpts, Port, IPS) @@ -160,6 +173,19 @@ init_tcp(PortIP, Module, Opts, SockOpts, Port, IPS) -> ListenSocket = listen_tcp(PortIP, Module, SockOpts, Port, IPS), %% Inform my parent that this port was opened succesfully proc_lib:init_ack({ok, self()}), + case erlang:function_exported(Module, tcp_init, 2) of + true -> + case catch Module:tcp_init(ListenSocket, Opts) of + {'EXIT', _} = Err -> + ?ERROR_MSG("failed to process callback function " + "~p:~s(~p, ~p): ~p", + [Module, tcp_init, ListenSocket, Opts, Err]); + _ -> + ok + end; + false -> + ok + end, %% And now start accepting connection attempts accept(ListenSocket, Module, Opts). @@ -342,6 +368,7 @@ start_listener2(Port, Module, Opts) -> %% But it doesn't hurt to attempt to start it for any listener. %% So, it's normal (and harmless) that in most cases this call returns: {error, {already_started, pid()}} maybe_start_stun(Module), + maybe_start_sip(Module), start_module_sup(Port, Module), start_listener_sup(Port, Module, Opts). @@ -463,6 +490,11 @@ maybe_start_stun(ejabberd_stun) -> maybe_start_stun(_) -> ok. +maybe_start_sip(esip_socket) -> + ejabberd:start_app(esip); +maybe_start_sip(_) -> + ok. + %%% %%% Check options %%% @@ -642,7 +674,11 @@ prepare_ip(IP) when is_binary(IP) -> prepare_mod(ejabberd_stun) -> prepare_mod(stun); +prepare_mod(ejabberd_sip) -> + prepare_mod(sip); prepare_mod(stun) -> stun; +prepare_mod(sip) -> + esip_socket; prepare_mod(Mod) when is_atom(Mod) -> Mod. diff --git a/src/mod_sip.erl b/src/mod_sip.erl new file mode 100644 index 000000000..cca91a33d --- /dev/null +++ b/src/mod_sip.erl @@ -0,0 +1,404 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2014, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 21 Apr 2014 by Evgeny Khramtsov +%%%------------------------------------------------------------------- +-module(mod_sip). + +-behaviour(gen_mod). +-behaviour(esip). + +%% API +-export([start/2, stop/1, prepare_request/1, make_response/2, + add_certfile/2, add_via/3]). + +%% esip_callbacks +-export([data_in/2, data_out/2, message_in/2, message_out/2, + request/2, request/3, response/2, locate/1]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). +-include("esip.hrl"). + +-record(sip_session, {us = {<<"">>, <<"">>} :: {binary(), binary()}, + socket = #sip_socket{}, + timestamp = now() :: erlang:timestamp(), + tref = make_ref() :: reference(), + expires = 0 :: non_neg_integer()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(_Host, _Opts) -> + ejabberd:start_app(esip), + esip:set_config_value(max_server_transactions, 10000), + esip:set_config_value(max_client_transactions, 10000), + esip:set_config_value(software, <<"ejabberd ", (?VERSION)/binary>>), + esip:set_config_value(module, ?MODULE), + Spec = {mod_sip_registrar, {mod_sip_registrar, start_link, []}, + transient, 2000, worker, [mod_sip_registrar]}, + TmpSupSpec = {mod_sip_proxy_sup, + {ejabberd_tmp_sup, start_link, + [mod_sip_proxy_sup, mod_sip_proxy]}, + permanent, infinity, supervisor, [ejabberd_tmp_sup]}, + supervisor:start_child(ejabberd_sup, Spec), + supervisor:start_child(ejabberd_sup, TmpSupSpec), + ok. + +stop(_Host) -> + ok. + +data_in(Data, #sip_socket{type = Transport, + addr = {MyIP, MyPort}, + peer = {PeerIP, PeerPort}}) -> + ?DEBUG( + "SIP [~p/in] ~s:~p -> ~s:~p:~n~s", + [Transport, inet_parse:ntoa(PeerIP), PeerPort, + inet_parse:ntoa(MyIP), MyPort, Data]). + +data_out(Data, #sip_socket{type = Transport, + addr = {MyIP, MyPort}, + peer = {PeerIP, PeerPort}}) -> + ?DEBUG( + "SIP [~p/out] ~s:~p -> ~s:~p:~n~s", + [Transport, inet_parse:ntoa(MyIP), MyPort, + inet_parse:ntoa(PeerIP), PeerPort, Data]). + +message_in(#sip{type = request, method = M} = Req, SIPSock) + when M /= <<"ACK">>, M /= <<"CANCEL">> -> + case action(Req, SIPSock) of + {relay, _LServer, _Opts} -> + ok; + Action -> + request(Req, SIPSock, undefined, Action) + end; +message_in(_, _) -> + ok. + +message_out(_, _) -> + ok. + +response(Resp, SIPSock) -> + case action(Resp, SIPSock) of + {relay, LServer, Opts} -> + case esip:split_hdrs('via', Resp#sip.hdrs) of + {[_], _} -> + ok; + {[_MyVia|Vias], TailHdrs} -> + %% TODO: check if MyVia is really my Via + NewResp = Resp#sip{hdrs = [{'via', Vias}|TailHdrs]}, + case proplists:get_value(socket, Opts) of + undefined -> + case esip:connect(NewResp, + add_certfile(LServer, Opts)) of + {ok, SIPSockOut} -> + esip:send(SIPSockOut, NewResp); + {error, _} -> + ok + end; + SIPSockOut -> + esip:send(SIPSockOut, NewResp) + end + end; + _ -> + ok + end. + +request(#sip{method = <<"ACK">>} = Req, SIPSock) -> + case action(Req, SIPSock) of + {relay, LServer, Opts} -> + Req1 = prepare_request(Req), + case esip:connect(Req1, add_certfile(LServer, Opts)) of + {ok, SIPSockOut} -> + Req2 = add_via(SIPSockOut, LServer, Req1), + esip:send(SIPSockOut, Req2); + {error, _} = Err -> + Err + end; + _ -> + pass + end; +request(#sip{method = <<"CANCEL">>} = Req, SIPSock) -> + case action(Req, SIPSock) of + loop -> + make_response(Req, #sip{status = 483, type = response}); + {unsupported, Require} -> + make_response(Req, #sip{status = 420, + type = response, + hdrs = [{'unsupported', + Require}]}); + {relay, LServer, Opts} -> + Req1 = prepare_request(Req), + case esip:connect(Req1, add_certfile(LServer, Opts)) of + {ok, SIPSockOut} -> + Req2 = add_via(SIPSockOut, LServer, Req1), + esip:send(SIPSockOut, Req2); + {error, _} = Err -> + Err + end, + pass; + _ -> + pass + end. + +request(Req, SIPSock, TrID) -> + request(Req, SIPSock, TrID, action(Req, SIPSock)). + +request(Req, SIPSock, TrID, Action) -> + case Action of + to_me -> + process(Req, SIPSock); + register -> + mod_sip_registrar:request(Req, SIPSock); + loop -> + make_response(Req, #sip{status = 483, type = response}); + {unsupported, Require} -> + make_response(Req, #sip{status = 420, + type = response, + hdrs = [{'unsupported', + Require}]}); + {relay, LServer, Opts} -> + case mod_sip_proxy:start(LServer, Opts) of + {ok, Pid} -> + mod_sip_proxy:route(Req, SIPSock, TrID, Pid), + {mod_sip_proxy, route, [Pid]}; + Err -> + ?INFO_MSG("failed to proxy request ~p: ~p", [Req, Err]), + Err + end; + {proxy_auth, Host} -> + make_response( + Req, + #sip{status = 407, + type = response, + hdrs = [{'proxy-authenticate', + make_auth_hdr(Host)}]}); + {auth, Host} -> + make_response( + Req, + #sip{status = 401, + type = response, + hdrs = [{'www-authenticate', + make_auth_hdr(Host)}]}); + deny -> + make_response(Req, #sip{status = 403, + type = response}); + not_found -> + make_response(Req, #sip{status = 480, + type = response}) + end. + +locate(_SIPMsg) -> + ok. + +find(#uri{user = User, host = Host}) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Host), + case mod_sip_registrar:find_session( + LUser, LServer) of + {ok, #sip_session{socket = Sock}} -> + {relay, LServer, [{socket, Sock}]}; + error -> + not_found + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +action(#sip{type = response, hdrs = Hdrs}, _SIPSock) -> + {_, ToURI, _} = esip:get_hdr('to', Hdrs), + {_, FromURI, _} = esip:get_hdr('from', Hdrs), + case at_my_host(FromURI) of + true -> + case at_my_host(ToURI) of + true -> + find(ToURI); + false -> + LServer = jlib:nameprep(FromURI#uri.host), + {relay, LServer, []} + end; + false -> + case at_my_host(ToURI) of + true -> + find(ToURI); + false -> + pass + end + end; +action(#sip{method = <<"REGISTER">>, type = request, hdrs = Hdrs, + uri = #uri{user = <<"">>} = URI} = Req, SIPSock) -> + case at_my_host(URI) of + true -> + case esip:get_hdrs('require', Hdrs) of + [_|_] = Require -> + {unsupported, Require}; + _ -> + {_, ToURI, _} = esip:get_hdr('to', Hdrs), + case at_my_host(ToURI) of + true -> + case check_auth(Req, 'authorization', SIPSock) of + true -> + register; + false -> + {auth, ToURI#uri.host} + end; + false -> + deny + end + end; + false -> + deny + end; +action(#sip{method = Method, hdrs = Hdrs, type = request} = Req, SIPSock) -> + case esip:get_hdr('max-forwards', Hdrs) of + 0 when Method == <<"OPTIONS">> -> + to_me; + 0 -> + loop; + _ -> + case esip:get_hdrs('proxy-require', Hdrs) of + [_|_] = Require -> + {unsupported, Require}; + _ -> + {_, ToURI, _} = esip:get_hdr('to', Hdrs), + {_, FromURI, _} = esip:get_hdr('from', Hdrs), + case at_my_host(FromURI) of + true -> + case check_auth(Req, 'proxy-authorization', SIPSock) of + true -> + case at_my_host(ToURI) of + true -> + find(ToURI); + false -> + LServer = jlib:nameprep(FromURI#uri.host), + {relay, LServer, []} + end; + false -> + {proxy_auth, FromURI#uri.host} + end; + false -> + case at_my_host(ToURI) of + true -> + find(ToURI); + false -> + deny + end + end + end + end. + +check_auth(#sip{method = <<"CANCEL">>}, _, _SIPSock) -> + true; +check_auth(#sip{method = Method, hdrs = Hdrs, body = Body}, AuthHdr, _SIPSock) -> + + Issuer = case AuthHdr of + 'authorization' -> + to; + 'proxy-authorization' -> + from + end, + {_, #uri{user = User, host = Host}, _} = esip:get_hdr(Issuer, Hdrs), + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Host), + case lists:filter( + fun({_, Params}) -> + Username = esip:get_param(<<"username">>, Params), + Realm = esip:get_param(<<"realm">>, Params), + (LUser == esip:unquote(Username)) + and (LServer == esip:unquote(Realm)) + end, esip:get_hdrs(AuthHdr, Hdrs)) of + [Auth|_] -> + case ejabberd_auth:get_password_s(LUser, LServer) of + <<"">> -> + false; + Password -> + esip:check_auth(Auth, Method, Body, Password) + end; + [] -> + false + end. + +allow() -> + [<<"OPTIONS">>, <<"REGISTER">>]. + +process(#sip{method = <<"OPTIONS">>} = Req, _) -> + make_response(Req, #sip{type = response, status = 200, + hdrs = [{'allow', allow()}]}); +process(#sip{method = <<"REGISTER">>} = Req, _) -> + make_response(Req, #sip{type = response, status = 400}); +process(Req, _) -> + make_response(Req, #sip{type = response, status = 405, + hdrs = [{'allow', allow()}]}). + +prepare_request(#sip{hdrs = Hdrs1} = Req) -> + MF = esip:get_hdr('max-forwards', Hdrs1), + Hdrs2 = esip:set_hdr('max-forwards', MF-1, Hdrs1), + Hdrs3 = lists:filter( + fun({'proxy-authorization', {_, Params}}) -> + Realm = esip:unquote(esip:get_param(<<"realm">>, Params)), + not is_my_host(jlib:nameprep(Realm)); + (_) -> + true + end, Hdrs2), + Req#sip{hdrs = Hdrs3}. + +make_auth_hdr(LServer) -> + Realm = jlib:nameprep(LServer), + {<<"Digest">>, [{<<"realm">>, esip:quote(Realm)}, + {<<"qop">>, esip:quote(<<"auth">>)}, + {<<"nonce">>, esip:quote(esip:make_hexstr(20))}]}. + +make_response(Req, Resp) -> + esip:make_response(Req, Resp, esip:make_tag()). + +at_my_host(#uri{host = Host}) -> + is_my_host(jlib:nameprep(Host)). + +is_my_host(LServer) -> + gen_mod:is_loaded(LServer, ?MODULE). + +add_certfile(LServer, Opts) -> + case ejabberd_config:get_option({domain_certfile, LServer}, + fun iolist_to_binary/1) of + CertFile when is_binary(CertFile), CertFile /= <<"">> -> + [{certfile, CertFile}|Opts]; + _ -> + Opts + end. + +add_via(#sip_socket{type = Transport}, LServer, #sip{hdrs = Hdrs} = Req) -> + ConfiguredVias = get_configured_vias(LServer), + {ViaHost, ViaPort} = proplists:get_value( + Transport, ConfiguredVias, {LServer, undefined}), + ViaTransport = case Transport of + tls -> <<"TLS">>; + tcp -> <<"TCP">>; + udp -> <<"UDP">> + end, + Via = #via{transport = ViaTransport, + host = ViaHost, + port = ViaPort, + params = [{<<"branch">>, esip:make_branch()}, + {<<"rport">>, <<"">>}]}, + Req#sip{hdrs = [{'via', [Via]}|Hdrs]}. + +get_configured_vias(LServer) -> + gen_mod:get_module_opt( + LServer, ?MODULE, via, + fun(L) -> + lists:map( + fun(Opts) -> + Type = proplists:get_value(type, Opts), + Host = proplists:get_value(host, Opts), + Port = proplists:get_value(port, Opts), + true = (Type == tcp) or (Type == tls) or (Type == udp), + true = is_binary(Host) and (Host /= <<"">>), + true = (is_integer(Port) + and (Port > 0) and (Port < 65536)) + or (Port == undefined), + {Type, {Host, Port}} + end, L) + end, []). diff --git a/src/mod_sip_proxy.erl b/src/mod_sip_proxy.erl new file mode 100644 index 000000000..aa749ccf7 --- /dev/null +++ b/src/mod_sip_proxy.erl @@ -0,0 +1,152 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2014, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 21 Apr 2014 by Evgeny Khramtsov +%%%------------------------------------------------------------------- +-module(mod_sip_proxy). + +-define(GEN_FSM, p1_fsm). +-behaviour(?GEN_FSM). + +%% API +-export([start/2, start_link/2, route/4, route/5]). + +%% gen_fsm callbacks +-export([init/1, wait_for_request/2, wait_for_response/2, + handle_event/3, handle_sync_event/4, + handle_info/3, terminate/3, code_change/4]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). +-include("esip.hrl"). + +-define(MAX_REDIRECTS, 5). + +-record(state, {host = <<"">> :: binary(), + opts = [] :: [{certfile, binary()}], + orig_trid, + orig_req :: #sip{}, + client_trid}). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(LServer, Opts) -> + supervisor:start_child(mod_sip_proxy_sup, [LServer, Opts]). + +start_link(LServer, Opts) -> + ?GEN_FSM:start_link(?MODULE, [LServer, Opts], []). + +route(Resp, Req, _SIPSock, TrID, Pid) -> + ?GEN_FSM:send_event(Pid, {Resp, Req, TrID}). + +route(SIPMsg, _SIPSock, TrID, Pid) -> + ?GEN_FSM:send_event(Pid, {SIPMsg, TrID}), + wait. + +%%%=================================================================== +%%% gen_fsm callbacks +%%%=================================================================== +init([Host, Opts]) -> + {ok, wait_for_request, #state{opts = Opts, host = Host}}. + +wait_for_request({#sip{type = request} = Req, TrID}, State) -> + Opts = mod_sip:add_certfile(State#state.host, State#state.opts), + Req1 = mod_sip:prepare_request(Req), + case connect(Req1, Opts) of + {ok, SIPSocket} -> + Req2 = mod_sip:add_via(SIPSocket, State#state.host, Req1), + case esip:request(SIPSocket, Req2, {?MODULE, route, [self()]}) of + {ok, ClientTrID} -> + {next_state, wait_for_response, + State#state{orig_trid = TrID, + orig_req = Req, + client_trid = ClientTrID}}; + Err -> + {Status, Reason} = esip:error_status(Err), + esip:reply(TrID, mod_sip:make_response( + Req, #sip{type = response, + status = Status, + reason = Reason})), + {stop, normal, State} + end; + Err -> + {Status, Reason} = esip:error_status(Err), + esip:reply(TrID, mod_sip:make_response( + Req, #sip{type = response, + status = Status, + reason = Reason})), + {stop, normal, State} + end; +wait_for_request(_Event, State) -> + {next_state, wait_for_request, State}. + +wait_for_response({#sip{method = <<"CANCEL">>, type = request}, _TrID}, State) -> + esip:cancel(State#state.client_trid), + {next_state, wait_for_response, State}; +wait_for_response({Resp, _TrID}, State) -> + case Resp of + {error, _} -> + Req = State#state.orig_req, + {Status, Reason} = esip:error_status(Resp), + case Status of + 408 when Req#sip.method /= <<"INVITE">> -> + %% Absorb useless 408. See RFC4320 + esip:stop_transaction(State#state.orig_trid); + _ -> + ErrResp = mod_sip:make_response( + Req, + #sip{type = response, + status = Status, + reason = Reason}), + esip:reply(State#state.orig_trid, ErrResp) + end, + {stop, normal, State}; + #sip{status = 100} -> + {next_state, wait_for_response, State}; + #sip{status = Status} -> + case esip:split_hdrs('via', Resp#sip.hdrs) of + {[_], _} -> + {stop, normal, State}; + {[_|Vias], NewHdrs} -> + esip:reply(State#state.orig_trid, + Resp#sip{hdrs = [{'via', Vias}|NewHdrs]}), + if Status < 200 -> + {next_state, wait_for_response, State}; + true -> + {stop, normal, State} + end + end + end; +wait_for_response(_Event, State) -> + {next_state, wait_for_response, State}. + +handle_event(_Event, StateName, State) -> + {next_state, StateName, State}. + +handle_sync_event(_Event, _From, StateName, State) -> + Reply = ok, + {reply, Reply, StateName, State}. + +handle_info(_Info, StateName, State) -> + {next_state, StateName, State}. + +terminate(_Reason, _StateName, _State) -> + ok. + +code_change(_OldVsn, StateName, State, _Extra) -> + {ok, StateName, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +connect(Req, Opts) -> + case proplists:get_value(socket, Opts) of + undefined -> + esip:connect(Req, Opts); + #sip_socket{} = SIPSock -> + {ok, SIPSock} + end. diff --git a/src/mod_sip_registrar.erl b/src/mod_sip_registrar.erl new file mode 100644 index 000000000..d8f485fef --- /dev/null +++ b/src/mod_sip_registrar.erl @@ -0,0 +1,196 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2014, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 23 Apr 2014 by Evgeny Khramtsov +%%%------------------------------------------------------------------- +-module(mod_sip_registrar). + +-define(GEN_SERVER, p1_server). +-behaviour(?GEN_SERVER). + +%% API +-export([start_link/0, request/2, find_session/2]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). +-include("esip.hrl"). + +-record(sip_session, {us = {<<"">>, <<"">>} :: {binary(), binary()}, + socket = #sip_socket{}, + timestamp = now() :: erlang:timestamp(), + tref = 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 = jlib:nodeprep(U), + LServer = jlib:nameprep(S), + {PeerIP, _} = SIPSock#sip_socket.peer, + US = {LUser, LServer}, + Expires = esip:get_hdr('expires', Hdrs, 0), + case esip:get_hdrs('contact', Hdrs) of + [<<"*">>] when Expires == 0 -> + ?INFO_MSG("unregister SIP session for user ~s@~s from ~s", + [LUser, LServer, inet_parse:ntoa(PeerIP)]), + unregister_session(US), + mod_sip:make_response(Req, #sip{type = response, status = 200}); + [{_, _URI, _Params}|_] = Contacts -> + ContactsWithExpires = + lists:map( + fun({Name, URI, Params}) -> + Exp = case to_integer( + esip:get_param( + <<"expires">>, Params), + 0, (1 bsl 32)-1) of + {ok, E} -> E; + _ -> Expires + end, + NewParams = esip:set_param( + <<"expires">>, + erlang:integer_to_binary(Exp), + Params), + {Exp, {Name, URI, NewParams}} + end, Contacts), + [{Expires1, _}|_] = lists:keysort(1, ContactsWithExpires), + MinExpires = min_expires(), + if Expires1 >= MinExpires -> + ?INFO_MSG("register SIP session for user ~s@~s from ~s", + [LUser, LServer, inet_parse:ntoa(PeerIP)]), + register_session(US, SIPSock, Expires1), + mod_sip:make_response( + Req, + #sip{type = response, + status = 200, + hdrs = [{'contact', + [C || {_, C} <- ContactsWithExpires]}]}); + Expires1 > 0, Expires1 < MinExpires -> + mod_sip:make_response( + Req, #sip{type = response, + status = 423, + hdrs = [{'min-expires', MinExpires}]}); + true -> + ?INFO_MSG("unregister SIP session for user ~s@~s from ~s", + [LUser, LServer, inet_parse:ntoa(PeerIP)]), + unregister_session(US), + mod_sip:make_response( + Req, + #sip{type = response, status = 200, + hdrs = [{'contact', + [C || {_, C} <- ContactsWithExpires]}]}) + end; + _ -> + mod_sip:make_response(Req, #sip{type = response, status = 400}) + end. + +find_session(U, S) -> + case mnesia:dirty_read(sip_session, {U, S}) of + [Session] -> + {ok, Session}; + [] -> + error + end. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([]) -> + mnesia:create_table(sip_session, + [{ram_copies, [node()]}, + {attributes, record_info(fields, sip_session)}]), + mnesia:add_table_copy(sip_session, node(), ram_copies), + {ok, #state{}}. + +handle_call({write, Session}, _From, State) -> + Res = write_session(Session), + {reply, Res, State}; +handle_call({delete, US}, _From, State) -> + Res = delete_session(US), + {reply, Res, State}; +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({write, Session}, State) -> + write_session(Session), + {noreply, State}; +handle_info({delete, US}, State) -> + delete_session(US), + {noreply, State}; +handle_info({timeout, TRef, US}, State) -> + case mnesia:dirty_read(sip_session, US) of + [#sip_session{tref = TRef}] -> + mnesia:dirty_delete(sip_session, US); + [] -> + ok + end, + {noreply, State}; +handle_info(_Info, State) -> + ?ERROR_MSG("got unexpected info: ~p", [_Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +register_session(US, SIPSocket, Expires) -> + Session = #sip_session{us = US, + socket = SIPSocket, + timestamp = now(), + expires = Expires}, + gen_server:call(?MODULE, {write, Session}). + +unregister_session(US) -> + gen_server:call(?MODULE, {delete, US}). + +write_session(#sip_session{us = US, expires = Expires} = Session) -> + case mnesia:dirty_read(sip_session, US) of + [#sip_session{tref = TRef}] -> + erlang:cancel_timer(TRef); + [] -> + ok + end, + NewTRef = erlang:start_timer(Expires * 1000, self(), US), + mnesia:dirty_write(Session#sip_session{tref = NewTRef}). + +delete_session(US) -> + case mnesia:dirty_read(sip_session, US) of + [#sip_session{tref = TRef}] -> + erlang:cancel_timer(TRef), + mnesia:dirty_delete(sip_session, US); + [] -> + ok + end. + +min_expires() -> + 60. + +to_integer(Bin, Min, Max) -> + case catch list_to_integer(binary_to_list(Bin)) of + N when N >= Min, N =< Max -> + {ok, N}; + _ -> + error + end. diff --git a/vars.config.in b/vars.config.in index 31c356fc9..037af96bc 100644 --- a/vars.config.in +++ b/vars.config.in @@ -28,6 +28,7 @@ {json, @json@}. {http, @http@}. {lager, @lager@}. +{sip, @sip@}. {iconv, @iconv@}. %% Version