]> granicus.if.org Git - ejabberd/commitdiff
SIP support
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>
Wed, 30 Apr 2014 15:20:38 +0000 (19:20 +0400)
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>
Wed, 30 Apr 2014 15:38:15 +0000 (19:38 +0400)
Conflicts:
configure
configure.ac
doc/guide.tex

configure
configure.ac
doc/guide.tex
doc/introduction.tex
rebar.config.script
src/ejabberd_listener.erl
src/mod_sip.erl [new file with mode: 0644]
src/mod_sip_proxy.erl [new file with mode: 0644]
src/mod_sip_registrar.erl [new file with mode: 0644]
vars.config.in

index 4114b2e696addad4b702baf263fe763c9467eccf..e26d6e894345ef3863cd72a1aa015a75aa3d2c81 100755 (executable)
--- 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 <ejabberd@process-one.net>.
 #
@@ -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\\"
 
index 853d91ecb7ef4e7f337e4a8e99726354763c13c8..6cb0580f904646c05bdc044fdc2ad9c770da0293 100644 (file)
@@ -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
index 21e66a0844b7cfbdaaa31cb3d4dd46efb018ba04..488ab6a3de1eb2f898c810a32d083fc20ed57954 100644 (file)
@@ -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}
 
index 163312b38fd8358a8f6a73447c11626cb6719c4f..fee27048c6940e4451ed7156fc16db6a37724ce0 100644 (file)
@@ -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}
index 7a5e332c45d277249af1566c0947dc13927a7853..dc68d1d30c0c6f2e9227b21cc143d739c7339192 100644 (file)
@@ -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),
index 2051afdb2e1a41b7b39f098734e9ce622beaac13..71f7440776a33f5bbd6eb114870b6976ad03d3e8 100644 (file)
@@ -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 (file)
index 0000000..cca91a3
--- /dev/null
@@ -0,0 +1,404 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2014, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 21 Apr 2014 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-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 (file)
index 0000000..aa749cc
--- /dev/null
@@ -0,0 +1,152 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2014, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 21 Apr 2014 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-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 (file)
index 0000000..d8f485f
--- /dev/null
@@ -0,0 +1,196 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2014, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 23 Apr 2014 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-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.
index 31c356fc9604e38f34f1b01f900f80620283f031..037af96bca7e7d9907327acb8d82341347945ffd 100644 (file)
@@ -28,6 +28,7 @@
 {json, @json@}.
 {http, @http@}.
 {lager, @lager@}.
+{sip, @sip@}.
 {iconv, @iconv@}.
 
 %% Version