diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/ejabberd_auth_odbc.erl | 1 | ||||
-rw-r--r-- | src/ejabberd_c2s.erl | 6 | ||||
-rw-r--r-- | src/ejabberd_ctl.erl | 1 | ||||
-rw-r--r-- | src/ejabberd_listener.erl | 36 | ||||
-rw-r--r-- | src/ejabberd_odbc.erl | 9 | ||||
-rw-r--r-- | src/ejabberd_odbc_sup.erl | 6 | ||||
-rw-r--r-- | src/ejabberd_s2s.erl | 154 | ||||
-rw-r--r-- | src/ejabberd_s2s_in.erl | 185 | ||||
-rw-r--r-- | src/ejabberd_s2s_out.erl | 106 | ||||
-rw-r--r-- | src/ejabberd_sm.erl | 17 | ||||
-rw-r--r-- | src/ejabberd_xmlrpc.erl | 6 | ||||
-rw-r--r-- | src/mod_register_web.erl | 18 | ||||
-rw-r--r-- | src/mod_sip.erl | 404 | ||||
-rw-r--r-- | src/mod_sip_proxy.erl | 152 | ||||
-rw-r--r-- | src/mod_sip_registrar.erl | 196 |
15 files changed, 1065 insertions, 232 deletions
diff --git a/src/ejabberd_auth_odbc.erl b/src/ejabberd_auth_odbc.erl index 57cef930a..27e89aa2b 100644 --- a/src/ejabberd_auth_odbc.erl +++ b/src/ejabberd_auth_odbc.erl @@ -212,6 +212,7 @@ is_user_exists(User, Server) -> true; %% Account exists {selected, [<<"password">>], []} -> false; %% Account does not exist + {error, unknownhost} -> false; {error, Error} -> {error, Error} catch _:B -> {error, B} diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 2cdf205cc..f2e16e15b 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -107,7 +107,7 @@ auth_module = unknown, ip, aux_fields = [], - lang}). + lang = <<"">>}). %-define(DBGFSM, true). @@ -1228,6 +1228,10 @@ handle_info(replaced, StateName, StateData) -> Lang = StateData#state.lang, Xmlelement = ?SERRT_CONFLICT(Lang, <<"Replaced by new connection">>), handle_info({kick, replaced, Xmlelement}, StateName, StateData); +handle_info(disconnect, StateName, StateData) -> + Lang = StateData#state.lang, + Xmlelement = ?SERRT_POLICY_VIOLATION(Lang, <<"has been kicked">>), + handle_info({kick, kicked_by_admin, Xmlelement}, StateName, StateData); handle_info({kick, Reason, Xmlelement}, _StateName, StateData) -> send_element(StateData, Xmlelement), send_trailer(StateData), diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index d8d067640..eda1c4970 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -418,6 +418,7 @@ is_supported_args(Args) -> fun({_Name, Format}) -> (Format == integer) or (Format == string) + or (Format == binary) end, Args). 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/ejabberd_odbc.erl b/src/ejabberd_odbc.erl index 6a7d9de6d..df9a4c398 100644 --- a/src/ejabberd_odbc.erl +++ b/src/ejabberd_odbc.erl @@ -140,9 +140,12 @@ sql_bloc(Host, F) -> sql_call(Host, {sql_bloc, F}). sql_call(Host, Msg) -> case get(?STATE_KEY) of undefined -> - (?GEN_FSM):sync_send_event(ejabberd_odbc_sup:get_random_pid(Host), - {sql_cmd, Msg, now()}, - ?TRANSACTION_TIMEOUT); + case ejabberd_odbc_sup:get_random_pid(Host) of + none -> {error, unknownhost}; + Pid -> + (?GEN_FSM):sync_send_event(Pid,{sql_cmd, Msg, now()}, + ?TRANSACTION_TIMEOUT) + end; _State -> nested_op(Msg) end. diff --git a/src/ejabberd_odbc_sup.erl b/src/ejabberd_odbc_sup.erl index bfad5428e..d05fd139e 100644 --- a/src/ejabberd_odbc_sup.erl +++ b/src/ejabberd_odbc_sup.erl @@ -82,8 +82,10 @@ get_pids(Host) -> [R#sql_pool.pid || R <- Rs]. get_random_pid(Host) -> - Pids = get_pids(Host), - lists:nth(erlang:phash(now(), length(Pids)), Pids). + case get_pids(Host) of + [] -> none; + Pids -> lists:nth(erlang:phash(now(), length(Pids)), Pids) + end. add_pid(Host, Pid) -> F = fun () -> diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index 057c60a98..eb9894350 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -37,7 +37,8 @@ incoming_s2s_number/0, outgoing_s2s_number/0, clean_temporarily_blocked_table/0, list_temporarily_blocked_hosts/0, - external_host_overloaded/1, is_temporarly_blocked/1]). + external_host_overloaded/1, is_temporarly_blocked/1, + check_peer_certificate/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, @@ -53,6 +54,14 @@ -include("ejabberd_commands.hrl"). +-include_lib("public_key/include/public_key.hrl"). + +-define(PKIXEXPLICIT, 'OTP-PUB-KEY'). + +-define(PKIXIMPLICIT, 'OTP-PUB-KEY'). + +-include("XmppAddr.hrl"). + -define(DEFAULT_MAX_S2S_CONNECTIONS_NUMBER, 1). -define(DEFAULT_MAX_S2S_CONNECTIONS_NUMBER_PER_NODE, 1). @@ -207,6 +216,31 @@ try_register(FromTo) -> dirty_get_connections() -> mnesia:dirty_all_keys(s2s). +check_peer_certificate(SockMod, Sock, Peer) -> + case SockMod:get_peer_certificate(Sock) of + {ok, Cert} -> + case SockMod:get_verify_result(Sock) of + 0 -> + case idna:domain_utf8_to_ascii(Peer) of + false -> + {error, <<"Cannot decode remote server name">>}; + AsciiPeer -> + case + lists:any(fun(D) -> match_domain(AsciiPeer, D) end, + get_cert_domains(Cert)) of + true -> + {ok, <<"Verification successful">>}; + false -> + {error, <<"Certificate host name mismatch">>} + end + end; + VerifyRes -> + {error, p1_tls:get_cert_verify_string(VerifyRes, Cert)} + end; + error -> + {error, <<"Cannot get peer certificate">>} + end. + %%==================================================================== %% gen_server callbacks %%==================================================================== @@ -619,3 +653,121 @@ get_s2s_state(S2sPid) -> {badrpc, _} -> [{status, error}] end, [{s2s_pid, S2sPid} | Infos]. + +get_cert_domains(Cert) -> + {rdnSequence, Subject} = + (Cert#'Certificate'.tbsCertificate)#'TBSCertificate'.subject, + Extensions = + (Cert#'Certificate'.tbsCertificate)#'TBSCertificate'.extensions, + lists:flatmap(fun (#'AttributeTypeAndValue'{type = + ?'id-at-commonName', + value = Val}) -> + case 'OTP-PUB-KEY':decode('X520CommonName', Val) of + {ok, {_, D1}} -> + D = if is_binary(D1) -> D1; + is_list(D1) -> list_to_binary(D1); + true -> error + end, + if D /= error -> + case jlib:string_to_jid(D) of + #jid{luser = <<"">>, lserver = LD, + lresource = <<"">>} -> + [LD]; + _ -> [] + end; + true -> [] + end; + _ -> [] + end; + (_) -> [] + end, + lists:flatten(Subject)) + ++ + lists:flatmap(fun (#'Extension'{extnID = + ?'id-ce-subjectAltName', + extnValue = Val}) -> + BVal = if is_list(Val) -> list_to_binary(Val); + true -> Val + end, + case 'OTP-PUB-KEY':decode('SubjectAltName', BVal) + of + {ok, SANs} -> + lists:flatmap(fun ({otherName, + #'AnotherName'{'type-id' = + ?'id-on-xmppAddr', + value = + XmppAddr}}) -> + case + 'XmppAddr':decode('XmppAddr', + XmppAddr) + of + {ok, D} + when + is_binary(D) -> + case + jlib:string_to_jid((D)) + of + #jid{luser = + <<"">>, + lserver = + LD, + lresource = + <<"">>} -> + case + idna:domain_utf8_to_ascii(LD) + of + false -> + []; + PCLD -> + [PCLD] + end; + _ -> [] + end; + _ -> [] + end; + ({dNSName, D}) + when is_list(D) -> + case + jlib:string_to_jid(list_to_binary(D)) + of + #jid{luser = <<"">>, + lserver = LD, + lresource = + <<"">>} -> + [LD]; + _ -> [] + end; + (_) -> [] + end, + SANs); + _ -> [] + end; + (_) -> [] + end, + Extensions). + +match_domain(Domain, Domain) -> true; +match_domain(Domain, Pattern) -> + DLabels = str:tokens(Domain, <<".">>), + PLabels = str:tokens(Pattern, <<".">>), + match_labels(DLabels, PLabels). + +match_labels([], []) -> true; +match_labels([], [_ | _]) -> false; +match_labels([_ | _], []) -> false; +match_labels([DL | DLabels], [PL | PLabels]) -> + case lists:all(fun (C) -> + $a =< C andalso C =< $z orelse + $0 =< C andalso C =< $9 orelse + C == $- orelse C == $* + end, + binary_to_list(PL)) + of + true -> + Regexp = ejabberd_regexp:sh_to_awk(PL), + case ejabberd_regexp:run(DL, Regexp) of + match -> match_labels(DLabels, PLabels); + nomatch -> false + end; + false -> false + end. diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl index 3eb0b71cc..4fde814fe 100644 --- a/src/ejabberd_s2s_in.erl +++ b/src/ejabberd_s2s_in.erl @@ -30,8 +30,7 @@ -behaviour(p1_fsm). %% External exports --export([start/2, start_link/2, match_domain/2, - socket_type/0]). +-export([start/2, start_link/2, socket_type/0]). %% gen_fsm callbacks -export([init/1, wait_for_stream/2, @@ -44,14 +43,6 @@ -include("jlib.hrl"). --include_lib("public_key/include/public_key.hrl"). - --define(PKIXEXPLICIT, 'OTP-PUB-KEY'). - --define(PKIXIMPLICIT, 'OTP-PUB-KEY'). - --include("XmppAddr.hrl"). - -define(DICT, dict). -record(state, @@ -227,45 +218,11 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, Auth = if StateData#state.tls_enabled -> case jlib:nameprep(xml:get_attr_s(<<"from">>, Attrs)) of From when From /= <<"">>, From /= error -> - case - (StateData#state.sockmod):get_peer_certificate(StateData#state.socket) - of - {ok, Cert} -> - case - (StateData#state.sockmod):get_verify_result(StateData#state.socket) - of - 0 -> - case - idna:domain_utf8_to_ascii(From) - of - false -> - {error, From, - <<"Cannot decode 'from' attribute">>}; - PCAuthDomain -> - case - lists:any(fun (D) -> - match_domain(PCAuthDomain, - D) - end, - get_cert_domains(Cert)) - of - true -> - {ok, From, - <<"Success">>}; - false -> - {error, From, - <<"Certificate host name mismatch">>} - end - end; - CertVerifyRes -> - {error, From, - p1_tls:get_cert_verify_string(CertVerifyRes, - Cert)} - end; - error -> - {error, From, - <<"Cannot get peer certificate">>} - end; + {Result, Message} = + ejabberd_s2s:check_peer_certificate(StateData#state.sockmod, + StateData#state.socket, + From), + {Result, From, Message}; _ -> {error, <<"(unknown)">>, <<"Got no valid 'from' attribute">>} @@ -294,15 +251,9 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, ?INFO_MSG("Closing s2s connection: ~s <--> ~s (~s)", [StateData#state.server, RemoteServer, CertError]), send_text(StateData, - xml:element_to_binary(?SERRT_POLICY_VIOLATION(<<"en">>, - CertError))), - {atomic, Pid} = - ejabberd_s2s:find_connection(jlib:make_jid(<<"">>, - Server, <<"">>), - jlib:make_jid(<<"">>, - RemoteServer, - <<"">>)), - ejabberd_s2s_out:stop_connection(Pid), + <<(xml:element_to_binary(?SERRT_POLICY_VIOLATION(<<"en">>, + CertError)))/binary, + (?STREAM_TRAILER)/binary>>), {stop, normal, StateData}; {VerifyResult, RemoteServer, Msg} -> {SASL, NewStateData} = case VerifyResult of @@ -746,124 +697,6 @@ is_key_packet(#xmlel{name = Name, attrs = Attrs, xml:get_attr_s(<<"id">>, Attrs), xml:get_cdata(Els)}; is_key_packet(_) -> false. -get_cert_domains(Cert) -> - {rdnSequence, Subject} = - (Cert#'Certificate'.tbsCertificate)#'TBSCertificate'.subject, - Extensions = - (Cert#'Certificate'.tbsCertificate)#'TBSCertificate'.extensions, - lists:flatmap(fun (#'AttributeTypeAndValue'{type = - ?'id-at-commonName', - value = Val}) -> - case 'OTP-PUB-KEY':decode('X520CommonName', Val) of - {ok, {_, D1}} -> - D = if is_binary(D1) -> D1; - is_list(D1) -> list_to_binary(D1); - true -> error - end, - if D /= error -> - case jlib:string_to_jid(D) of - #jid{luser = <<"">>, lserver = LD, - lresource = <<"">>} -> - [LD]; - _ -> [] - end; - true -> [] - end; - _ -> [] - end; - (_) -> [] - end, - lists:flatten(Subject)) - ++ - lists:flatmap(fun (#'Extension'{extnID = - ?'id-ce-subjectAltName', - extnValue = Val}) -> - BVal = if is_list(Val) -> list_to_binary(Val); - true -> Val - end, - case 'OTP-PUB-KEY':decode('SubjectAltName', BVal) - of - {ok, SANs} -> - lists:flatmap(fun ({otherName, - #'AnotherName'{'type-id' = - ?'id-on-xmppAddr', - value = - XmppAddr}}) -> - case - 'XmppAddr':decode('XmppAddr', - XmppAddr) - of - {ok, D} - when - is_binary(D) -> - case - jlib:string_to_jid((D)) - of - #jid{luser = - <<"">>, - lserver = - LD, - lresource = - <<"">>} -> - case - idna:domain_utf8_to_ascii(LD) - of - false -> - []; - PCLD -> - [PCLD] - end; - _ -> [] - end; - _ -> [] - end; - ({dNSName, D}) - when is_list(D) -> - case - jlib:string_to_jid(list_to_binary(D)) - of - #jid{luser = <<"">>, - lserver = LD, - lresource = - <<"">>} -> - [LD]; - _ -> [] - end; - (_) -> [] - end, - SANs); - _ -> [] - end; - (_) -> [] - end, - Extensions). - -match_domain(Domain, Domain) -> true; -match_domain(Domain, Pattern) -> - DLabels = str:tokens(Domain, <<".">>), - PLabels = str:tokens(Pattern, <<".">>), - match_labels(DLabels, PLabels). - -match_labels([], []) -> true; -match_labels([], [_ | _]) -> false; -match_labels([_ | _], []) -> false; -match_labels([DL | DLabels], [PL | PLabels]) -> - case lists:all(fun (C) -> - $a =< C andalso C =< $z orelse - $0 =< C andalso C =< $9 orelse - C == $- orelse C == $* - end, - binary_to_list(PL)) - of - true -> - Regexp = ejabberd_regexp:sh_to_awk(PL), - case ejabberd_regexp:run(DL, Regexp) of - match -> match_labels(DLabels, PLabels); - nomatch -> false - end; - false -> false - end. - fsm_limit_opts(Opts) -> case lists:keysearch(max_fsm_queue, 1, Opts) of {value, {_, N}} when is_integer(N) -> [{max_queue, N}]; diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index a0a83631d..052729314 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -69,6 +69,7 @@ use_v10 = true :: boolean(), tls = false :: boolean(), tls_required = false :: boolean(), + tls_certverify = false :: boolean(), tls_enabled = false :: boolean(), tls_options = [connect] :: list(), authenticated = false :: boolean(), @@ -160,28 +161,27 @@ stop_connection(Pid) -> p1_fsm:send_event(Pid, closed). init([From, Server, Type]) -> process_flag(trap_exit, true), ?DEBUG("started: ~p", [{From, Server, Type}]), - {TLS, TLSRequired} = case - ejabberd_config:get_option( - s2s_use_starttls, - fun(true) -> true; - (false) -> false; - (optional) -> optional; - (required) -> required; - (required_trusted) -> required_trusted - end) - of - UseTls - when (UseTls == undefined) or - (UseTls == false) -> - {false, false}; - UseTls - when (UseTls == true) or (UseTls == optional) -> - {true, false}; - UseTls - when (UseTls == required) or - (UseTls == required_trusted) -> - {true, true} - end, + {TLS, TLSRequired, TLSCertverify} = + case ejabberd_config:get_option( + s2s_use_starttls, + fun(true) -> true; + (false) -> false; + (optional) -> optional; + (required) -> required; + (required_trusted) -> required_trusted + end) + of + UseTls + when (UseTls == undefined) or (UseTls == false) -> + {false, false, false}; + UseTls + when (UseTls == true) or (UseTls == optional) -> + {true, false, false}; + required -> + {true, true, false}; + required_trusted -> + {true, true, true} + end, UseV10 = TLS, TLSOpts1 = case ejabberd_config:get_option( @@ -223,9 +223,9 @@ init([From, Server, Type]) -> Timer = erlang:start_timer(?S2STIMEOUT, self(), []), {ok, open_socket, #state{use_v10 = UseV10, tls = TLS, - tls_required = TLSRequired, tls_options = TLSOpts, - queue = queue:new(), myname = From, server = Server, - new = New, verify = Verify, timer = Timer}}. + tls_required = TLSRequired, tls_certverify = TLSCertverify, + tls_options = TLSOpts, queue = queue:new(), myname = From, + server = Server, new = New, verify = Verify, timer = Timer}}. %%---------------------------------------------------------------------- %% Func: StateName/2 @@ -345,35 +345,57 @@ open_socket2(Type, Addr, Port) -> wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> + {CertCheckRes, CertCheckMsg, NewStateData} = + if StateData#state.tls_certverify, StateData#state.tls_enabled -> + {Res, Msg} = + ejabberd_s2s:check_peer_certificate(ejabberd_socket, + StateData#state.socket, + StateData#state.server), + ?DEBUG("Certificate verification result for ~s: ~s", + [StateData#state.server, Msg]), + {Res, Msg, StateData#state{tls_certverify = false}}; + true -> + {no_verify, <<"Not verified">>, StateData} + end, case {xml:get_attr_s(<<"xmlns">>, Attrs), xml:get_attr_s(<<"xmlns:db">>, Attrs), xml:get_attr_s(<<"version">>, Attrs) == <<"1.0">>} of + _ when CertCheckRes == error -> + send_text(NewStateData, + <<(xml:element_to_binary(?SERRT_POLICY_VIOLATION(<<"en">>, + CertCheckMsg)))/binary, + (?STREAM_TRAILER)/binary>>), + ?INFO_MSG("Closing s2s connection: ~s -> ~s (~s)", + [NewStateData#state.myname, + NewStateData#state.server, + CertCheckMsg]), + {stop, normal, NewStateData}; {<<"jabber:server">>, <<"jabber:server:dialback">>, false} -> - send_db_request(StateData); + send_db_request(NewStateData); {<<"jabber:server">>, <<"jabber:server:dialback">>, true} - when StateData#state.use_v10 -> - {next_state, wait_for_features, StateData, ?FSMTIMEOUT}; + when NewStateData#state.use_v10 -> + {next_state, wait_for_features, NewStateData, ?FSMTIMEOUT}; %% Clause added to handle Tigase's workaround for an old ejabberd bug: {<<"jabber:server">>, <<"jabber:server:dialback">>, true} - when not StateData#state.use_v10 -> - send_db_request(StateData); + when not NewStateData#state.use_v10 -> + send_db_request(NewStateData); {<<"jabber:server">>, <<"">>, true} - when StateData#state.use_v10 -> + when NewStateData#state.use_v10 -> {next_state, wait_for_features, - StateData#state{db_enabled = false}, ?FSMTIMEOUT}; + NewStateData#state{db_enabled = false}, ?FSMTIMEOUT}; {NSProvided, DB, _} -> - send_text(StateData, ?INVALID_NAMESPACE_ERR), + send_text(NewStateData, ?INVALID_NAMESPACE_ERR), ?INFO_MSG("Closing s2s connection: ~s -> ~s (invalid " "namespace).~nNamespace provided: ~p~nNamespac" "e expected: \"jabber:server\"~nxmlns:db " "provided: ~p~nAll attributes: ~p", - [StateData#state.myname, StateData#state.server, + [NewStateData#state.myname, NewStateData#state.server, NSProvided, DB, Attrs]), - {stop, normal, StateData} + {stop, normal, NewStateData} end; wait_for_stream({xmlstreamerror, _}, StateData) -> send_text(StateData, @@ -570,15 +592,19 @@ wait_for_features({xmlstreamelement, El}, StateData) -> if not SASLEXT and not StartTLS and StateData#state.authenticated -> send_queue(StateData, StateData#state.queue), - ?INFO_MSG("Connection established: ~s -> ~s", - [StateData#state.myname, StateData#state.server]), + ?INFO_MSG("Connection established: ~s -> ~s with " + "SASL EXTERNAL and TLS=~p", + [StateData#state.myname, StateData#state.server, + StateData#state.tls_enabled]), ejabberd_hooks:run(s2s_connect_hook, [StateData#state.myname, StateData#state.server]), {next_state, stream_established, StateData#state{queue = queue:new()}}; SASLEXT and StateData#state.try_auth and - (StateData#state.new /= false) -> + (StateData#state.new /= false) and + (StateData#state.tls_enabled or + not StateData#state.tls_required) -> send_element(StateData, #xmlel{name = <<"auth">>, attrs = @@ -736,8 +762,8 @@ wait_for_starttls_proceed({xmlstreamelement, El}, tls_options = TLSOpts}, send_text(NewStateData, io_lib:format(?STREAM_HEADER, - [StateData#state.myname, - StateData#state.server, + [NewStateData#state.myname, + NewStateData#state.server, <<" version='1.0'">>])), {next_state, wait_for_stream, NewStateData, ?FSMTIMEOUT}; diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index b1673d4b9..58debf0c1 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -52,6 +52,7 @@ connected_users/0, connected_users_number/0, user_resources/2, + disconnect_user/2, get_session_pid/3, get_user_info/3, get_user_ip/3, @@ -801,7 +802,13 @@ commands() -> desc = "List user's connected resources", module = ?MODULE, function = user_resources, args = [{user, binary}, {host, binary}], - result = {resources, {list, {resource, string}}}}]. + result = {resources, {list, {resource, string}}}}, + #ejabberd_commands{name = disconnect_user, + tags = [session], + desc = "Disconnect user's active sessions", + module = ?MODULE, function = disconnect_user, + args = [{user, binary}, {host, binary}], + result = {num_resources, integer}}]. -spec connected_users() -> [binary()]. @@ -818,6 +825,14 @@ user_resources(User, Server) -> Resources = get_user_resources(User, Server), lists:sort(Resources). +disconnect_user(User, Server) -> + Resources = get_user_resources(User, Server), + lists:foreach( + fun(Resource) -> + PID = get_session_pid(User, Server, Resource), + PID ! disconnect + end, Resources), + length(Resources). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Update Mnesia tables diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl index 76610af8e..ff89d2858 100644 --- a/src/ejabberd_xmlrpc.erl +++ b/src/ejabberd_xmlrpc.erl @@ -428,7 +428,11 @@ format_arg({array, Elements}, {list, ElementsDef}) format_arg(Arg, integer) when is_integer(Arg) -> Arg; format_arg(Arg, binary) when is_list(Arg) -> list_to_binary(Arg); format_arg(Arg, binary) when is_binary(Arg) -> Arg; -format_arg(Arg, string) when is_binary(Arg) -> Arg. +format_arg(Arg, string) when is_list(Arg) -> list_to_binary(Arg); +format_arg(Arg, string) when is_binary(Arg) -> Arg; +format_arg(Arg, Format) -> + ?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]), + throw({error_formatting_argument, Arg, Format}). %% ----------------------------- %% Result diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl index 0da53b26d..ee3ff0069 100644 --- a/src/mod_register_web.erl +++ b/src/mod_register_web.erl @@ -140,7 +140,10 @@ process([<<"change_password">>], list_to_binary([?T(<<"There was an error changing the password: ">>), ?T(get_error_text(Error))]), {404, [], ErrorText} - end. + end; + +process(Path, _Request) -> + {404, [], "Not Found"}. %%%---------------------------------------------------------------------- %%% CSS @@ -487,12 +490,13 @@ register_account(Username, Host, Password) -> Access = gen_mod:get_module_opt(Host, mod_register, access, fun(A) when is_atom(A) -> A end, all), - JID = jlib:make_jid(Username, Host, <<"">>), - Match = acl:match_rule(Host, Access, JID), - case {JID, Match} of - {error, _} -> {error, invalid_jid}; - {_, deny} -> {error, not_allowed}; - {_, allow} -> register_account2(Username, Host, Password) + case jlib:make_jid(Username, Host, <<"">>) of + error -> {error, invalid_jid}; + JID -> + case acl:match_rule(Host, Access, JID) of + deny -> {error, not_allowed}; + allow -> register_account2(Username, Host, Password) + end end. register_account2(Username, Host, Password) -> 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 <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 index 000000000..aa749ccf7 --- /dev/null +++ b/src/mod_sip_proxy.erl @@ -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 index 000000000..d8f485fef --- /dev/null +++ b/src/mod_sip_registrar.erl @@ -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. |