diff options
Diffstat (limited to 'src')
62 files changed, 7039 insertions, 1571 deletions
diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 957aa5d46..fabe2d3e0 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -58,12 +58,14 @@ start(normal, _Args) -> Sup = ejabberd_sup:start_link(), ejabberd_rdbms:start(), ejabberd_riak_sup:start(), + ejabberd_sm:start(), ejabberd_auth:start(), cyrsasl:start(), % Profiling %ejabberd_debug:eprof_start(), %ejabberd_debug:fprof_start(), maybe_add_nameservers(), + ext_mod:start(), start_modules(), ejabberd_listener:start_listeners(), ?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]), @@ -108,6 +110,7 @@ loop() -> end. db_init() -> + ejabberd_config:env_binary_to_list(mnesia, dir), MyNode = node(), DbNodes = mnesia:system_info(db_nodes), case lists:member(MyNode, DbNodes) of @@ -236,6 +239,7 @@ set_loglevel_from_config() -> ejabberd_logger:set(Level). start_apps() -> + crypto:start(), ejabberd:start_app(sasl), ejabberd:start_app(ssl), ejabberd:start_app(p1_yaml), diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index 34d4a52b2..9985dd3de 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -71,7 +71,7 @@ -callback get_vh_registered_users(binary(), opts()) -> [{binary(), binary()}]. -callback get_vh_registered_users_number(binary()) -> number(). -callback get_vh_registered_users_number(binary(), opts()) -> number(). --callback get_password(binary(), binary()) -> false | binary(). +-callback get_password(binary(), binary()) -> false | binary() | {binary(), binary(), binary(), integer()}. -callback get_password_s(binary(), binary()) -> binary(). start() -> @@ -267,7 +267,7 @@ get_vh_registered_users_number(Server, Opts) -> end, auth_modules(Server))). --spec get_password(binary(), binary()) -> false | binary(). +-spec get_password(binary(), binary()) -> false | binary() | {binary(), binary(), binary(), integer()}. get_password(User, Server) -> lists:foldl(fun (M, false) -> @@ -425,6 +425,10 @@ auth_modules() -> %% Return the list of authenticated modules for a given host auth_modules(Server) -> LServer = jlib:nameprep(Server), + Default = case gen_mod:default_db(LServer) of + mnesia -> internal; + DBType -> DBType + end, Methods = ejabberd_config:get_option( {auth_method, LServer}, fun(V) when is_list(V) -> @@ -432,7 +436,7 @@ auth_modules(Server) -> V; (V) when is_atom(V) -> [V] - end, []), + end, [Default]), [jlib:binary_to_atom(<<"ejabberd_auth_", (jlib:atom_to_binary(M))/binary>>) || M <- Methods]. diff --git a/src/ejabberd_auth_odbc.erl b/src/ejabberd_auth_odbc.erl index aea039c1b..881b86cca 100644 --- a/src/ejabberd_auth_odbc.erl +++ b/src/ejabberd_auth_odbc.erl @@ -38,101 +38,186 @@ get_vh_registered_users_number/2, get_password/2, get_password_s/2, is_user_exists/2, remove_user/2, remove_user/3, store_type/0, - plain_password_required/0]). + plain_password_required/0, + convert_to_scram/1]). -include("ejabberd.hrl"). -include("logger.hrl"). +-define(SALT_LENGTH, 16). + %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(_Host) -> ok. -plain_password_required() -> false. +plain_password_required() -> + case is_scrammed() of + false -> false; + true -> true + end. -store_type() -> plain. +store_type() -> + case is_scrammed() of + false -> plain; %% allows: PLAIN DIGEST-MD5 SCRAM + true -> scram %% allows: PLAIN SCRAM + end. %% @spec (User, Server, Password) -> true | false | {error, Error} check_password(User, Server, Password) -> - case jlib:nodeprep(User) of - error -> false; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - try odbc_queries:get_password(LServer, Username) of - {selected, [<<"password">>], [[Password]]} -> - Password /= <<"">>; - {selected, [<<"password">>], [[_Password2]]} -> - false; %% Password is not correct - {selected, [<<"password">>], []} -> - false; %% Account does not exist - {error, _Error} -> - false %% Typical error is that table doesn't exist - catch - _:_ -> - false %% Typical error is database not accessible - end + LServer = jlib:nameprep(Server), + LUser = jlib:nodeprep(User), + if (LUser == error) or (LServer == error) -> + false; + (LUser == <<>>) or (LServer == <<>>) -> + false; + true -> + Username = ejabberd_odbc:escape(LUser), + case is_scrammed() of + true -> + try odbc_queries:get_password_scram(LServer, Username) of + {selected, [<<"password">>, <<"serverkey">>, + <<"salt">>, <<"iterationcount">>], + [[StoredKey, ServerKey, Salt, IterationCount]]} -> + Scram = + #scram{storedkey = StoredKey, + serverkey = ServerKey, + salt = Salt, + iterationcount = jlib:binary_to_integer( + IterationCount)}, + is_password_scram_valid(Password, Scram); + {selected, [<<"password">>, <<"serverkey">>, + <<"salt">>, <<"iterationcount">>], []} -> + false; %% Account does not exist + {error, _Error} -> + false %% Typical error is that table doesn't exist + catch + _:_ -> + false %% Typical error is database not accessible + end; + false -> + try odbc_queries:get_password(LServer, Username) of + {selected, [<<"password">>], [[Password]]} -> + Password /= <<"">>; + {selected, [<<"password">>], [[_Password2]]} -> + false; %% Password is not correct + {selected, [<<"password">>], []} -> + false; %% Account does not exist + {error, _Error} -> + false %% Typical error is that table doesn't exist + catch + _:_ -> + false %% Typical error is database not accessible + end + end end. %% @spec (User, Server, Password, Digest, DigestGen) -> true | false | {error, Error} check_password(User, Server, Password, Digest, DigestGen) -> - case jlib:nodeprep(User) of - error -> false; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - try odbc_queries:get_password(LServer, Username) of - %% Account exists, check if password is valid - {selected, [<<"password">>], [[Passwd]]} -> - DigRes = if Digest /= <<"">> -> - Digest == DigestGen(Passwd); - true -> false - end, - if DigRes -> true; - true -> (Passwd == Password) and (Password /= <<"">>) - end; - {selected, [<<"password">>], []} -> - false; %% Account does not exist - {error, _Error} -> - false %% Typical error is that table doesn't exist - catch - _:_ -> - false %% Typical error is database not accessible - end + LServer = jlib:nameprep(Server), + LUser = jlib:nodeprep(User), + if (LUser == error) or (LServer == error) -> + false; + (LUser == <<>>) or (LServer == <<>>) -> + false; + true -> + case is_scrammed() of + false -> + Username = ejabberd_odbc:escape(LUser), + try odbc_queries:get_password(LServer, Username) of + %% Account exists, check if password is valid + {selected, [<<"password">>], [[Passwd]]} -> + DigRes = if Digest /= <<"">> -> + Digest == DigestGen(Passwd); + true -> false + end, + if DigRes -> true; + true -> (Passwd == Password) and (Password /= <<"">>) + end; + {selected, [<<"password">>], []} -> + false; %% Account does not exist + {error, _Error} -> + false %% Typical error is that table doesn't exist + catch + _:_ -> + false %% Typical error is database not accessible + end; + true -> + false + end end. %% @spec (User::string(), Server::string(), Password::string()) -> %% ok | {error, invalid_jid} set_password(User, Server, Password) -> - case jlib:nodeprep(User) of - error -> {error, invalid_jid}; - LUser -> - Username = ejabberd_odbc:escape(LUser), - Pass = ejabberd_odbc:escape(Password), - LServer = jlib:nameprep(Server), - case catch odbc_queries:set_password_t(LServer, - Username, Pass) - of - {atomic, ok} -> ok; - Other -> {error, Other} - end + LServer = jlib:nameprep(Server), + LUser = jlib:nodeprep(User), + if (LUser == error) or (LServer == error) -> + {error, invalid_jid}; + (LUser == <<>>) or (LServer == <<>>) -> + {error, invalid_jid}; + true -> + Username = ejabberd_odbc:escape(LUser), + case is_scrammed() of + true -> + Scram = password_to_scram(Password), + case catch odbc_queries:set_password_scram_t( + LServer, + Username, + ejabberd_odbc:escape(Scram#scram.storedkey), + ejabberd_odbc:escape(Scram#scram.serverkey), + ejabberd_odbc:escape(Scram#scram.salt), + jlib:integer_to_binary(Scram#scram.iterationcount) + ) + of + {atomic, ok} -> ok; + Other -> {error, Other} + end; + false -> + Pass = ejabberd_odbc:escape(Password), + case catch odbc_queries:set_password_t(LServer, + Username, Pass) + of + {atomic, ok} -> ok; + Other -> {error, Other} + end + end end. %% @spec (User, Server, Password) -> {atomic, ok} | {atomic, exists} | {error, invalid_jid} try_register(User, Server, Password) -> - case jlib:nodeprep(User) of - error -> {error, invalid_jid}; - LUser -> + LServer = jlib:nameprep(Server), + LUser = jlib:nodeprep(User), + if (LUser == error) or (LServer == error) -> + {error, invalid_jid}; + (LUser == <<>>) or (LServer == <<>>) -> + {error, invalid_jid}; + true -> Username = ejabberd_odbc:escape(LUser), - Pass = ejabberd_odbc:escape(Password), - LServer = jlib:nameprep(Server), - case catch odbc_queries:add_user(LServer, Username, - Pass) - of - {updated, 1} -> {atomic, ok}; - _ -> {atomic, exists} - end + case is_scrammed() of + true -> + Scram = password_to_scram(Password), + case catch odbc_queries:add_user_scram( + LServer, + Username, + ejabberd_odbc:escape(Scram#scram.storedkey), + ejabberd_odbc:escape(Scram#scram.serverkey), + ejabberd_odbc:escape(Scram#scram.salt), + jlib:integer_to_binary(Scram#scram.iterationcount) + ) of + {updated, 1} -> {atomic, ok}; + _ -> {atomic, exists} + end; + false -> + Pass = ejabberd_odbc:escape(Password), + case catch odbc_queries:add_user(LServer, Username, + Pass) + of + {updated, 1} -> {atomic, ok}; + _ -> {atomic, exists} + end + end end. dirty_get_registered_users() -> @@ -175,29 +260,53 @@ get_vh_registered_users_number(Server, Opts) -> end. get_password(User, Server) -> - case jlib:nodeprep(User) of - error -> false; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - case catch odbc_queries:get_password(LServer, Username) - of - {selected, [<<"password">>], [[Password]]} -> Password; - _ -> false - end + LServer = jlib:nameprep(Server), + LUser = jlib:nodeprep(User), + if (LUser == error) or (LServer == error) -> + false; + (LUser == <<>>) or (LServer == <<>>) -> + false; + true -> + Username = ejabberd_odbc:escape(LUser), + case is_scrammed() of + true -> + case catch odbc_queries:get_password_scram( + LServer, Username) of + {selected, [<<"password">>, <<"serverkey">>, + <<"salt">>, <<"iterationcount">>], + [[StoredKey, ServerKey, Salt, IterationCount]]} -> + {jlib:decode_base64(StoredKey), + jlib:decode_base64(ServerKey), + jlib:decode_base64(Salt), + jlib:binary_to_integer(IterationCount)}; + _ -> false + end; + false -> + case catch odbc_queries:get_password(LServer, Username) + of + {selected, [<<"password">>], [[Password]]} -> Password; + _ -> false + end + end end. get_password_s(User, Server) -> - case jlib:nodeprep(User) of - error -> <<"">>; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - case catch odbc_queries:get_password(LServer, Username) - of - {selected, [<<"password">>], [[Password]]} -> Password; - _ -> <<"">> - end + LServer = jlib:nameprep(Server), + LUser = jlib:nodeprep(User), + if (LUser == error) or (LServer == error) -> + <<"">>; + (LUser == <<>>) or (LServer == <<>>) -> + <<"">>; + true -> + case is_scrammed() of + false -> + Username = ejabberd_odbc:escape(LUser), + case catch odbc_queries:get_password(LServer, Username) of + {selected, [<<"password">>], [[Password]]} -> Password; + _ -> <<"">> + end; + true -> <<"">> + end end. %% @spec (User, Server) -> true | false | {error, Error} @@ -234,23 +343,127 @@ remove_user(User, Server) -> %% @spec (User, Server, Password) -> ok | error | not_exists | not_allowed %% @doc Remove user if the provided password is correct. remove_user(User, Server, Password) -> - case jlib:nodeprep(User) of - error -> error; - LUser -> - Username = ejabberd_odbc:escape(LUser), - Pass = ejabberd_odbc:escape(Password), - LServer = jlib:nameprep(Server), - F = fun () -> - Result = odbc_queries:del_user_return_password(LServer, - Username, - Pass), - case Result of - {selected, [<<"password">>], [[Password]]} -> ok; - {selected, [<<"password">>], []} -> not_exists; - _ -> not_allowed - end - end, - {atomic, Result} = odbc_queries:sql_transaction(LServer, - F), - Result + LServer = jlib:nameprep(Server), + LUser = jlib:nodeprep(User), + if (LUser == error) or (LServer == error) -> + error; + (LUser == <<>>) or (LServer == <<>>) -> + error; + true -> + case is_scrammed() of + true -> + case check_password(User, Server, Password) of + true -> + remove_user(User, Server), + ok; + false -> not_allowed + end; + false -> + Username = ejabberd_odbc:escape(LUser), + Pass = ejabberd_odbc:escape(Password), + F = fun () -> + Result = odbc_queries:del_user_return_password( + LServer, Username, Pass), + case Result of + {selected, [<<"password">>], + [[Password]]} -> ok; + {selected, [<<"password">>], + []} -> not_exists; + _ -> not_allowed + end + end, + {atomic, Result} = odbc_queries:sql_transaction( + LServer, F), + Result + end + end. + +%%% +%%% SCRAM +%%% + +is_scrammed() -> + scram == + ejabberd_config:get_option({auth_password_format, ?MYNAME}, + fun(V) -> V end). + +password_to_scram(Password) -> + password_to_scram(Password, + ?SCRAM_DEFAULT_ITERATION_COUNT). + +password_to_scram(Password, IterationCount) -> + Salt = crypto:rand_bytes(?SALT_LENGTH), + SaltedPassword = scram:salted_password(Password, Salt, + IterationCount), + StoredKey = + scram:stored_key(scram:client_key(SaltedPassword)), + ServerKey = scram:server_key(SaltedPassword), + #scram{storedkey = jlib:encode_base64(StoredKey), + serverkey = jlib:encode_base64(ServerKey), + salt = jlib:encode_base64(Salt), + iterationcount = IterationCount}. + +is_password_scram_valid(Password, Scram) -> + IterationCount = Scram#scram.iterationcount, + Salt = jlib:decode_base64(Scram#scram.salt), + SaltedPassword = scram:salted_password(Password, Salt, + IterationCount), + StoredKey = + scram:stored_key(scram:client_key(SaltedPassword)), + jlib:decode_base64(Scram#scram.storedkey) == StoredKey. + +-define(BATCH_SIZE, 1000). + +set_password_scram_t(Username, + StoredKey, ServerKey, Salt, IterationCount) -> + odbc_queries:update_t(<<"users">>, + [<<"username">>, + <<"password">>, + <<"serverkey">>, + <<"salt">>, + <<"iterationcount">>], + [Username, StoredKey, + ServerKey, Salt, + IterationCount], + [<<"username='">>, Username, + <<"'">>]). + +convert_to_scram(Server) -> + LServer = jlib:nameprep(Server), + if + LServer == error; + LServer == <<>> -> + {error, {incorrect_server_name, Server}}; + true -> + F = fun () -> + case ejabberd_odbc:sql_query_t( + [<<"select username, password from users where " + "iterationcount=0 limit ">>, + jlib:integer_to_binary(?BATCH_SIZE), + <<";">>]) of + {selected, [<<"username">>, <<"password">>], []} -> + ok; + {selected, [<<"username">>, <<"password">>], Rs} -> + lists:foreach( + fun([LUser, Password]) -> + Username = ejabberd_odbc:escape(LUser), + Scram = password_to_scram(Password), + set_password_scram_t( + Username, + ejabberd_odbc:escape(Scram#scram.storedkey), + ejabberd_odbc:escape(Scram#scram.serverkey), + ejabberd_odbc:escape(Scram#scram.salt), + jlib:integer_to_binary(Scram#scram.iterationcount) + ) + end, Rs), + continue; + Err -> {bad_reply, Err} + end + end, + case odbc_queries:sql_transaction(LServer, F) of + {atomic, ok} -> ok; + {atomic, continue} -> convert_to_scram(Server); + {atomic, Error} -> {error, Error}; + Error -> Error + end end. diff --git a/src/ejabberd_auth_riak.erl b/src/ejabberd_auth_riak.erl index 5d631f497..3f3484c14 100644 --- a/src/ejabberd_auth_riak.erl +++ b/src/ejabberd_auth_riak.erl @@ -17,10 +17,9 @@ %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% -%%% You should have received a copy of the GNU General Public License -%%% along with this program; if not, write to the Free Software -%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA -%%% 02111-1307 USA +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 0855da219..4603af998 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -1679,38 +1679,27 @@ handle_info({route, From, To, Packet, in) of allow -> {true, Attrs, StateData}; - deny -> {false, Attrs, StateData} + deny -> + Err = + jlib:make_error_reply(Packet, + ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(To, From, + Err), + {false, Attrs, StateData} end; _ -> {true, Attrs, StateData} end, - if Pass == exit -> - %% When Pass==exit, NewState contains a string instead of a #state{} - Lang = StateData#state.lang, - send_element(StateData, ?SERRT_CONFLICT(Lang, NewState)), - send_trailer(StateData), - {stop, normal, StateData}; - Pass -> + if Pass -> Attrs2 = jlib:replace_from_to_attrs(jlib:jid_to_string(From), jlib:jid_to_string(To), NewAttrs), FixedPacket = #xmlel{name = Name, attrs = Attrs2, children = Els}, - FinalState = - case ejabberd_hooks:run_fold(c2s_filter_packet_in, - NewState#state.server, FixedPacket, - [NewState#state.jid, From, To]) - of - drop -> - NewState; - FinalPacket = #xmlel{} -> - SentState = send_packet(NewState, FinalPacket), - ejabberd_hooks:run(user_receive_packet, - SentState#state.server, - [SentState#state.jid, From, To, - FinalPacket]), - SentState - end, + SentStateData = send_packet(NewState, FixedPacket), + ejabberd_hooks:run(user_receive_packet, + SentStateData#state.server, + [SentStateData#state.jid, From, To, FixedPacket]), ejabberd_hooks:run(c2s_loop_debug, [{route, From, To, Packet}]), - fsm_next_state(StateName, FinalState); + fsm_next_state(StateName, SentStateData); true -> ejabberd_hooks:run(c2s_loop_debug, [{route, From, To, Packet}]), fsm_next_state(StateName, NewState) @@ -1737,6 +1726,10 @@ handle_info(system_shutdown, StateName, StateData) -> ok end, {stop, normal, StateData}; +handle_info({route_xmlstreamelement, El}, _StateName, StateData) -> + {next_state, NStateName, NStateData, _Timeout} = + session_established({xmlstreamelement, El}, StateData), + fsm_next_state(NStateName, NStateData); handle_info({force_update_presence, LUser}, StateName, #state{user = LUser, server = LServer} = StateData) -> NewStateData = case StateData#state.pres_last of @@ -1805,7 +1798,7 @@ print_state(State = #state{pres_t = T, pres_f = F, pres_a = A}) -> pres_f = {pres_f, ?SETS:size(F)}, pres_a = {pres_a, ?SETS:size(A)} }. - + %%---------------------------------------------------------------------- %% Func: terminate/3 %% Purpose: Shutdown the fsm @@ -1888,7 +1881,7 @@ send_text(StateData, Text) when StateData#state.mgmt_state == pending -> ?DEBUG("Cannot send text while waiting for resumption: ~p", [Text]); send_text(StateData, Text) when StateData#state.xml_socket -> ?DEBUG("Send Text on stream = ~p", [Text]), - (StateData#state.sockmod):send_xml(StateData#state.socket, + (StateData#state.sockmod):send_xml(StateData#state.socket, {xmlstreamraw, Text}); send_text(StateData, Text) when StateData#state.mgmt_state == active -> ?DEBUG("Send XML on stream = ~p", [Text]), @@ -2031,7 +2024,6 @@ get_conn_type(StateData) -> gen_tcp -> c2s_compressed; p1_tls -> c2s_compressed_tls end; - ejabberd_http_poll -> http_poll; ejabberd_http_bind -> http_bind; _ -> unknown end. @@ -2223,14 +2215,16 @@ try_roster_subscribe(Type, User, Server, From, To, Packet, StateData) -> presence_broadcast(StateData, From, JIDSet, Packet) -> JIDs = ?SETS:to_list(JIDSet), JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs, out), - send_multiple(StateData, From, JIDs2, Packet). + Server = StateData#state.server, + send_multiple(From, Server, JIDs2, Packet). %% Send presence when updating presence presence_broadcast_to_trusted(StateData, From, Trusted, JIDSet, Packet) -> JIDs = ?SETS:to_list(JIDSet), JIDs_trusted = [JID || JID <- JIDs, ?SETS:is_element(JID, Trusted)], JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs_trusted, out), - send_multiple(StateData, From, JIDs2, Packet). + Server = StateData#state.server, + send_multiple(From, Server, JIDs2, Packet). %% Send presence when connecting presence_broadcast_first(From, StateData, Packet) -> @@ -2242,7 +2236,7 @@ presence_broadcast_first(From, StateData, Packet) -> PacketProbe = #xmlel{name = <<"presence">>, attrs = [{<<"type">>,<<"probe">>}], children = []}, JIDs2Probe = format_and_check_privacy(From, StateData, PacketProbe, JIDsProbe, out), Server = StateData#state.server, - send_multiple(StateData, From, JIDs2Probe, PacketProbe), + send_multiple(From, Server, JIDs2Probe, PacketProbe), {As, JIDs} = ?SETS:fold( fun(JID, {A, JID_list}) -> @@ -2251,8 +2245,7 @@ presence_broadcast_first(From, StateData, Packet) -> {StateData#state.pres_a, []}, StateData#state.pres_f), JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs, out), - Server = StateData#state.server, - send_multiple(StateData, From, JIDs2, Packet), + send_multiple(From, Server, JIDs2, Packet), StateData#state{pres_a = As}. format_and_check_privacy(From, StateData, Packet, JIDs, Dir) -> @@ -2273,16 +2266,8 @@ format_and_check_privacy(From, StateData, Packet, JIDs, Dir) -> end, FJIDs). -send_multiple(StateData, From, JIDs, Packet) -> - lists:foreach( - fun(JID) -> - case privacy_check_packet(StateData, From, JID, Packet, out) of - deny -> - ok; - allow -> - ejabberd_router:route(From, JID, Packet) - end - end, JIDs). +send_multiple(From, Server, JIDs, Packet) -> + ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). remove_element(E, Set) -> case (?SETS):is_element(E, Set) of @@ -2848,9 +2833,12 @@ send_stanza_and_ack_req(StateData, Stanza) -> AckReq = #xmlel{name = <<"r">>, attrs = [{<<"xmlns">>, StateData#state.mgmt_xmlns}], children = []}, - StanzaS = xml:element_to_binary(Stanza), - AckReqS = xml:element_to_binary(AckReq), - send_text(StateData, [StanzaS, AckReqS]). + case send_element(StateData, Stanza) of + ok -> + send_element(StateData, AckReq); + error -> + error + end. mgmt_queue_add(StateData, El) -> NewNum = case StateData#state.mgmt_stanzas_out of diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index c279f2d0f..a4f38e836 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -317,7 +317,13 @@ execute_command2(Command, Arguments) -> Module = Command#ejabberd_commands.module, Function = Command#ejabberd_commands.function, ?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]), - apply(Module, Function, Arguments). + try apply(Module, Function, Arguments) of + Response -> + Response + catch + Problem -> + {error, Problem} + end. -spec get_tags_commands() -> [{string(), [string()]}]. diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 53936744e..5d1df5056 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -35,7 +35,8 @@ get_version/0, get_myhosts/0, get_mylang/0, prepare_opt_val/4, convert_table_to_binary/5, transform_options/1, collect_options/1, - convert_to_yaml/1, convert_to_yaml/2]). + convert_to_yaml/1, convert_to_yaml/2, + env_binary_to_list/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -84,7 +85,7 @@ start() -> %% If not specified, the default value 'ejabberd.yml' is assumed. %% @spec () -> string() get_ejabberd_config_path() -> - case application:get_env(config) of + case get_env_config() of {ok, Path} -> Path; undefined -> case os:getenv("EJABBERD_CONFIG_PATH") of @@ -95,6 +96,18 @@ get_ejabberd_config_path() -> end end. +-spec get_env_config() -> {ok, string()} | undefined. +get_env_config() -> + %% First case: the filename can be specified with: erl -config "/path/to/ejabberd.yml". + case application:get_env(config) of + R = {ok, _Path} -> R; + undefined -> + %% Second case for embbeding ejabberd in another app, for example for Elixir: + %% config :ejabberd, + %% file: "config/ejabberd.yml" + application:get_env(ejabberd, file) + end. + %% @doc Read the ejabberd configuration file. %% It also includes additional configuration files and replaces macros. %% This function will crash if finds some error in the configuration file. @@ -155,6 +168,22 @@ convert_to_yaml(File, Output) -> file:write_file(FileName, Data) end. +%% Some Erlang apps expects env parameters to be list and not binary. +%% For example, Mnesia is not able to start if mnesia dir is passed as a binary. +%% However, binary is most common on Elixir, so it is easy to make a setup mistake. +-spec env_binary_to_list(atom(), atom()) -> {ok, any()}|undefined. +env_binary_to_list(Application, Parameter) -> + %% Application need to be loaded to allow setting parameters + application:load(Application), + case application:get_env(Application, Parameter) of + {ok, Val} when is_binary(Val) -> + BVal = binary_to_list(Val), + application:set_env(Application, Parameter, BVal), + {ok, BVal}; + Other -> + Other + end. + %% @doc Read an ejabberd configuration file and return the terms. %% Input is an absolute or relative path to an ejabberd config file. %% Returns a list of plain terms, @@ -185,7 +214,7 @@ get_plain_terms_file(File1, Opts) -> consult(File) -> case filename:extension(File) of - ".yml" -> + Ex when (Ex == ".yml") or (Ex == ".yaml") -> case p1_yaml:decode_from_file(File, [plain_as_atom]) of {ok, []} -> {ok, []}; @@ -207,12 +236,10 @@ consult(File) -> end end. -parserl([$>, $\s | String]) -> - {ok, A2, _} = erl_scan:string(String), +parserl(<<"> ", Term/binary>>) -> + {ok, A2, _} = erl_scan:string(binary_to_list(Term)), {ok, A3} = erl_parse:parse_term(A2), A3; -parserl(B) when is_binary(B) -> - parserl(binary_to_list(B)); parserl({A, B}) -> {parserl(A), parserl(B)}; parserl([El|Tail]) -> @@ -681,7 +708,10 @@ is_file_readable(Path) -> end. get_version() -> - list_to_binary(element(2, application:get_key(ejabberd, vsn))). + case application:get_key(ejabberd, vsn) of + undefined -> ""; + {ok, Vsn} -> list_to_binary(Vsn) + end. -spec get_myhosts() -> [binary()]. @@ -709,26 +739,40 @@ replace_module(mod_roster_odbc) -> {mod_roster, odbc}; replace_module(mod_shared_roster_odbc) -> {mod_shared_roster, odbc}; replace_module(mod_vcard_odbc) -> {mod_vcard, odbc}; replace_module(mod_vcard_xupdate_odbc) -> {mod_vcard_xupdate, odbc}; -replace_module(Module) -> Module. - -replace_modules(Modules) -> - lists:map( - fun({Module, Opts}) -> - case replace_module(Module) of - {NewModule, DBType} -> - emit_deprecation_warning(Module, NewModule, DBType), - NewOpts = [{db_type, DBType} | - lists:keydelete(db_type, 1, Opts)], - {NewModule, transform_module_options(Module, NewOpts)}; - NewModule -> - if Module /= NewModule -> - emit_deprecation_warning(Module, NewModule); - true -> - ok - end, - {NewModule, transform_module_options(Module, Opts)} - end - end, Modules). +replace_module(Module) -> + case is_elixir_module(Module) of + true -> expand_elixir_module(Module); + false -> Module + end. + +replace_modules(Modules) -> lists:map( fun({Module, Opts}) -> case + replace_module(Module) of {NewModule, DBType} -> + emit_deprecation_warning(Module, NewModule, DBType), NewOpts = + [{db_type, DBType} | lists:keydelete(db_type, 1, Opts)], + {NewModule, transform_module_options(Module, NewOpts)}; NewModule + -> if Module /= NewModule -> emit_deprecation_warning(Module, + NewModule); true -> ok end, {NewModule, + transform_module_options(Module, Opts)} end end, Modules). + +%% Elixir module naming +%% ==================== + +%% If module name start with uppercase letter, this is an Elixir module: +is_elixir_module(Module) -> + case atom_to_list(Module) of + [H|_] when H >= 65, H =< 90 -> true; + _ ->false + end. + +%% We assume we know this is an elixir module +expand_elixir_module(Module) -> + case atom_to_list(Module) of + %% Module name already specified as an Elixir from Erlang module name + "Elixir." ++ _ -> Module; + %% if start with uppercase letter, this is an Elixir module: Append 'Elixir.' to module name. + ModuleString -> + list_to_atom("Elixir." ++ ModuleString) + end. strings_to_binary([]) -> []; @@ -1011,5 +1055,10 @@ emit_deprecation_warning(Module, NewModule, DBType) -> " instead", [Module, NewModule, DBType]). emit_deprecation_warning(Module, NewModule) -> - ?WARNING_MSG("Module ~s is deprecated, use ~s instead", - [Module, NewModule]). + case is_elixir_module(NewModule) of + %% Do not emit deprecation warning for Elixir + true -> ok; + false -> + ?WARNING_MSG("Module ~s is deprecated, use ~s instead", + [Module, NewModule]) + end. diff --git a/src/ejabberd_hooks.erl b/src/ejabberd_hooks.erl index c1cdefcb2..d136ba705 100644 --- a/src/ejabberd_hooks.erl +++ b/src/ejabberd_hooks.erl @@ -32,18 +32,21 @@ -export([start_link/0, add/3, add/4, + add/5, add_dist/5, + add_dist/6, delete/3, delete/4, - delete_dist/5, - run/2, - run_fold/3, - add/5, - add_dist/6, delete/5, + delete_dist/5, delete_dist/6, + run/2, run/3, - run_fold/4]). + run_fold/3, + run_fold/4, + get_handlers/2]). + +-export([delete_all_hooks/0]). %% gen_server callbacks -export([init/1, @@ -53,13 +56,14 @@ handle_info/2, terminate/2]). --include("ejabberd.hrl"). -include("logger.hrl"). %% Timeout of 5 seconds in calls to distributed hooks -define(TIMEOUT_DISTRIBUTED_HOOK, 5000). -record(state, {}). +-type local_hook() :: { Seq :: integer(), Module :: atom(), Function :: atom()}. +-type distributed_hook() :: { Seq :: integer(), Node :: atom(), Module :: atom(), Function :: atom()}. %%%---------------------------------------------------------------------- %%% API @@ -67,13 +71,13 @@ start_link() -> gen_server:start_link({local, ejabberd_hooks}, ejabberd_hooks, [], []). --spec add(atom(), fun(), number()) -> any(). +-spec add(atom(), fun(), number()) -> ok. %% @doc See add/4. add(Hook, Function, Seq) when is_function(Function) -> add(Hook, global, undefined, Function, Seq). --spec add(atom(), binary() | atom(), fun() | atom() , number()) -> any(). +-spec add(atom(), HostOrModule :: binary() | atom(), fun() | atom() , number()) -> ok. add(Hook, Host, Function, Seq) when is_function(Function) -> add(Hook, Host, undefined, Function, Seq); @@ -82,17 +86,17 @@ add(Hook, Host, Function, Seq) when is_function(Function) -> add(Hook, Module, Function, Seq) -> add(Hook, global, Module, Function, Seq). --spec add(atom(), binary() | global, atom(), atom() | fun(), number()) -> any(). +-spec add(atom(), binary() | global, atom(), atom() | fun(), number()) -> ok. add(Hook, Host, Module, Function, Seq) -> gen_server:call(ejabberd_hooks, {add, Hook, Host, Module, Function, Seq}). --spec add_dist(atom(), atom(), atom(), atom() | fun(), number()) -> any(). +-spec add_dist(atom(), atom(), atom(), atom() | fun(), number()) -> ok. add_dist(Hook, Node, Module, Function, Seq) -> gen_server:call(ejabberd_hooks, {add, Hook, global, Node, Module, Function, Seq}). --spec add_dist(atom(), binary() | global, atom(), atom(), atom() | fun(), number()) -> any(). +-spec add_dist(atom(), binary() | global, atom(), atom(), atom() | fun(), number()) -> ok. add_dist(Hook, Host, Node, Module, Function, Seq) -> gen_server:call(ejabberd_hooks, {add, Hook, Host, Node, Module, Function, Seq}). @@ -128,6 +132,17 @@ delete_dist(Hook, Node, Module, Function, Seq) -> delete_dist(Hook, Host, Node, Module, Function, Seq) -> gen_server:call(ejabberd_hooks, {delete, Hook, Host, Node, Module, Function, Seq}). +-spec delete_all_hooks() -> true. + +%% @doc Primarily for testing / instrumentation +delete_all_hooks() -> + gen_server:call(ejabberd_hooks, {delete_all}). + +-spec get_handlers(atom(), binary() | global) -> [local_hook() | distributed_hook()]. +%% @doc Returns currently set handler for hook name +get_handlers(Hookname, Host) -> + gen_server:call(ejabberd_hooks, {get_handlers, Hookname, Host}). + -spec run(atom(), list()) -> ok. %% @doc Run the calls of this hook in order, don't care about function results. @@ -190,65 +205,70 @@ init([]) -> %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_call({add, Hook, Host, Module, Function, Seq}, _From, State) -> - Reply = case ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> - El = {Seq, Module, Function}, - case lists:member(El, Ls) of - true -> - ok; - false -> - NewLs = lists:merge(Ls, [El]), - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok - end; - [] -> - NewLs = [{Seq, Module, Function}], - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok - end, + HookFormat = {Seq, Module, Function}, + Reply = handle_add(Hook, Host, HookFormat), {reply, Reply, State}; handle_call({add, Hook, Host, Node, Module, Function, Seq}, _From, State) -> - Reply = case ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> - El = {Seq, Node, Module, Function}, - case lists:member(El, Ls) of - true -> - ok; - false -> - NewLs = lists:merge(Ls, [El]), - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok - end; - [] -> - NewLs = [{Seq, Node, Module, Function}], - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok - end, + HookFormat = {Seq, Node, Module, Function}, + Reply = handle_add(Hook, Host, HookFormat), {reply, Reply, State}; + handle_call({delete, Hook, Host, Module, Function, Seq}, _From, State) -> - Reply = case ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> - NewLs = lists:delete({Seq, Module, Function}, Ls), - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok; - [] -> - ok - end, + HookFormat = {Seq, Module, Function}, + Reply = handle_delete(Hook, Host, HookFormat), {reply, Reply, State}; handle_call({delete, Hook, Host, Node, Module, Function, Seq}, _From, State) -> + HookFormat = {Seq, Node, Module, Function}, + Reply = handle_delete(Hook, Host, HookFormat), + {reply, Reply, State}; + +handle_call({get_handlers, Hook, Host}, _From, State) -> Reply = case ets:lookup(hooks, {Hook, Host}) of - [{_, Ls}] -> - NewLs = lists:delete({Seq, Node, Module, Function}, Ls), - ets:insert(hooks, {{Hook, Host}, NewLs}), - ok; - [] -> - ok - end, + [{_, Handlers}] -> Handlers; + [] -> [] + end, + {reply, Reply, State}; + +handle_call({delete_all}, _From, State) -> + Reply = ets:delete_all_objects(hooks), {reply, Reply, State}; + handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. +-spec handle_add(atom(), atom(), local_hook() | distributed_hook()) -> ok. +%% in-memory storage operation: Handle adding hook in ETS table +handle_add(Hook, Host, El) -> + case ets:lookup(hooks, {Hook, Host}) of + [{_, Ls}] -> + case lists:member(El, Ls) of + true -> + ok; + false -> + NewLs = lists:merge(Ls, [El]), + ets:insert(hooks, {{Hook, Host}, NewLs}), + ok + end; + [] -> + NewLs = [El], + ets:insert(hooks, {{Hook, Host}, NewLs}), + ok + end. + + +-spec handle_delete(atom(), atom(), local_hook() | distributed_hook()) -> ok. +%% in-memory storage operation: Handle deleting hook from ETS table +handle_delete(Hook, Host, El) -> + case ets:lookup(hooks, {Hook, Host}) of + [{_, Ls}] -> + NewLs = lists:delete(El, Ls), + ets:insert(hooks, {{Hook, Host}, NewLs}), + ok; + [] -> + ok + end. + %%---------------------------------------------------------------------- %% Func: handle_cast/2 %% Returns: {noreply, State} | @@ -275,7 +295,6 @@ handle_info(_Info, State) -> terminate(_Reason, _State) -> ok. - code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -283,9 +302,14 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%---------------------------------------------------------------------- +-spec run1([local_hook()|distributed_hook()], atom(), list()) -> ok. + run1([], _Hook, _Args) -> ok; +%% Run distributed hook on target node. +%% It is not attempted again in case of failure. Next hook will be executed run1([{_Seq, Node, Module, Function} | Ls], Hook, Args) -> + %% MR: Should we have a safe rpc, like we have a safe apply or is bad_rpc enough ? case rpc:call(Node, Module, Function, Args, ?TIMEOUT_DISTRIBUTED_HOOK) of timeout -> ?ERROR_MSG("Timeout on RPC to ~p~nrunning hook: ~p", @@ -305,15 +329,10 @@ run1([{_Seq, Node, Module, Function} | Ls], Hook, Args) -> run1(Ls, Hook, Args) end; run1([{_Seq, Module, Function} | Ls], Hook, Args) -> - Res = if is_function(Function) -> - catch apply(Function, Args); - true -> - catch apply(Module, Function, Args) - end, + Res = safe_apply(Module, Function, Args), case Res of {'EXIT', Reason} -> - ?ERROR_MSG("~p~nrunning hook: ~p", - [Reason, {Hook, Args}]), + ?ERROR_MSG("~p~nrunning hook: ~p", [Reason, {Hook, Args}]), run1(Ls, Hook, Args); stop -> ok; @@ -346,15 +365,10 @@ run_fold1([{_Seq, Node, Module, Function} | Ls], Hook, Val, Args) -> run_fold1(Ls, Hook, NewVal, Args) end; run_fold1([{_Seq, Module, Function} | Ls], Hook, Val, Args) -> - Res = if is_function(Function) -> - catch apply(Function, [Val | Args]); - true -> - catch apply(Module, Function, [Val | Args]) - end, + Res = safe_apply(Module, Function, [Val | Args]), case Res of {'EXIT', Reason} -> - ?ERROR_MSG("~p~nrunning hook: ~p", - [Reason, {Hook, Args}]), + ?ERROR_MSG("~p~nrunning hook: ~p", [Reason, {Hook, Args}]), run_fold1(Ls, Hook, Val, Args); stop -> stopped; @@ -363,3 +377,10 @@ run_fold1([{_Seq, Module, Function} | Ls], Hook, Val, Args) -> NewVal -> run_fold1(Ls, Hook, NewVal, Args) end. + +safe_apply(Module, Function, Args) -> + if is_function(Function) -> + catch apply(Function, Args); + true -> + catch apply(Module, Function, Args) + end. diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index 289d54127..4e7f4b554 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -56,7 +56,7 @@ %% to have the module test_web handle requests with %% paths starting with "/test/module": %% - %% {5280, ejabberd_http, [http_poll, web_admin, + %% {5280, ejabberd_http, [http_bind, web_admin, %% {request_handlers, [{["test", "module"], mod_test_web}]}]} %% request_handlers = [], @@ -94,14 +94,24 @@ start_link(SockData, Opts) -> init({SockMod, Socket}, Opts) -> TLSEnabled = proplists:get_bool(tls, Opts), TLSOpts1 = lists:filter(fun ({certfile, _}) -> true; + ({ciphers, _}) -> true; (_) -> false end, Opts), - TLSOpts2 = case proplists:get_bool(tls_compression, Opts) of - false -> [compression_none | TLSOpts1]; - true -> TLSOpts1 + TLSOpts2 = case lists:keysearch(protocol_options, 1, Opts) of + {value, {_, O}} -> + [_|ProtocolOptions] = lists:foldl( + fun(X, Acc) -> X ++ Acc end, [], + [["|" | binary_to_list(Opt)] || Opt <- O, is_binary(Opt)] + ), + [{protocol_options, iolist_to_binary(ProtocolOptions)} | TLSOpts1]; + _ -> TLSOpts1 end, - TLSOpts = [verify_none | TLSOpts2], + TLSOpts3 = case proplists:get_bool(tls_compression, Opts) of + false -> [compression_none | TLSOpts2]; + true -> TLSOpts2 + end, + TLSOpts = [verify_none | TLSOpts3], {SockMod1, Socket1} = if TLSEnabled -> inet:setopts(Socket, [{recbuf, 8192}]), {ok, TLSSocket} = p1_tls:tcp_to_tls(Socket, @@ -109,11 +119,6 @@ init({SockMod, Socket}, Opts) -> {p1_tls, TLSSocket}; true -> {SockMod, Socket} end, - case SockMod1 of - gen_tcp -> - inet:setopts(Socket1, [{packet, http_bin}, {recbuf, 8192}]); - _ -> ok - end, Captcha = case proplists:get_bool(captcha, Opts) of true -> [{[<<"captcha">>], ejabberd_captcha}]; false -> [] @@ -130,10 +135,6 @@ init({SockMod, Socket}, Opts) -> true -> [{[<<"http-bind">>], mod_http_bind}]; false -> [] end, - Poll = case proplists:get_bool(http_poll, Opts) of - true -> [{[<<"http-poll">>], ejabberd_http_poll}]; - false -> [] - end, XMLRPC = case proplists:get_bool(xmlrpc, Opts) of true -> [{[], ejabberd_xmlrpc}]; false -> [] @@ -146,7 +147,7 @@ init({SockMod, Socket}, Opts) -> Mod} || {Path, Mod} <- Hs] end, []), RequestHandlers = DefinedHandlers ++ Captcha ++ Register ++ - Admin ++ Bind ++ Poll ++ XMLRPC, + Admin ++ Bind ++ XMLRPC, ?DEBUG("S: ~p~n", [RequestHandlers]), DefaultHost = gen_mod:get_opt(default_host, Opts, fun(A) -> A end, undefined), @@ -182,22 +183,10 @@ receive_headers(#state{trail = Trail} = State) -> SockMod = State#state.sockmod, Socket = State#state.socket, Data = SockMod:recv(Socket, 0, 300000), - case State#state.sockmod of - gen_tcp -> - NewState = process_header(State, Data), - case NewState#state.end_of_request of - true -> - ok; - _ -> - receive_headers(NewState) - end; - _ -> - case Data of - {ok, D} -> - parse_headers(State#state{trail = <<Trail/binary, D/binary>>}); - {error, _} -> - ok - end + case Data of + {error, _} -> ok; + {ok, D} -> + parse_headers(State#state{trail = <<Trail/binary, D/binary>>}) end. parse_headers(#state{trail = <<>>} = State) -> @@ -270,6 +259,11 @@ process_header(State, Data) -> {ok, {http_header, _, 'Host' = Name, _, Host}} -> State#state{request_host = Host, request_headers = add_header(Name, Host, State)}; + {ok, {http_header, _, Name, _, Value}} when is_binary(Name) -> + State#state{request_headers = + add_header(normalize_header_name(Name), + Value, + State)}; {ok, {http_header, _, Name, _, Value}} -> State#state{request_headers = add_header(Name, Value, State)}; @@ -280,7 +274,7 @@ process_header(State, Data) -> []), throw(http_request_no_host_header); {ok, http_eoh} -> - ?DEBUG("(~w) http query: ~w ~s~n", + ?DEBUG("(~w) http query: ~w ~p~n", [State#state.socket, State#state.request_method, element(2, State#state.request_path)]), {HostProvided, Port, TP} = @@ -294,21 +288,20 @@ process_header(State, Data) -> send_text(State2, Out), case State2#state.request_keepalive of true -> - case SockMod of - gen_tcp -> inet:setopts(Socket, [{packet, http_bin}]); - _ -> ok - end, #state{sockmod = SockMod, socket = Socket, options = State#state.options, + default_host = State#state.default_host, request_handlers = State#state.request_handlers}; _ -> #state{end_of_request = true, options = State#state.options, + default_host = State#state.default_host, request_handlers = State#state.request_handlers} end; _ -> #state{end_of_request = true, options = State#state.options, + default_host = State#state.default_host, request_handlers = State#state.request_handlers} end. @@ -345,48 +338,93 @@ get_transfer_protocol(SockMod, HostPort) -> %% XXX bard: search through request handlers looking for one that %% matches the requested URL path, and pass control to it. If none is %% found, answer with HTTP 404. -process([], _) -> - ejabberd_web:error(not_found); -process(Handlers, Request) -> - %% Only the first element in the path prefix is checked - [{HandlerPathPrefix, HandlerModule} | HandlersLeft] = - Handlers, - case lists:prefix(HandlerPathPrefix, - Request#request.path) - or (HandlerPathPrefix == Request#request.path) - of - true -> - ?DEBUG("~p matches ~p", - [Request#request.path, HandlerPathPrefix]), - LocalPath = lists:nthtail(length(HandlerPathPrefix), - Request#request.path), - ?DEBUG("~p", [Request#request.headers]), - R = HandlerModule:process(LocalPath, Request), - ejabberd_hooks:run(http_request_debug, - [{LocalPath, Request}]), - R; - false -> process(HandlersLeft, Request) + +process([], _, _, _, _) -> ejabberd_web:error(not_found); +process(Handlers, Request, Socket, SockMod, Trail) -> + {HandlerPathPrefix, HandlerModule, HandlerOpts, HandlersLeft} = + case Handlers of + [{Pfx, Mod} | Tail] -> + {Pfx, Mod, [], Tail}; + [{Pfx, Mod, Opts} | Tail] -> + {Pfx, Mod, Opts, Tail} + end, + + case (lists:prefix(HandlerPathPrefix, Request#request.path) or + (HandlerPathPrefix==Request#request.path)) of + true -> + ?DEBUG("~p matches ~p", [Request#request.path, HandlerPathPrefix]), + %% LocalPath is the path "local to the handler", i.e. if + %% the handler was registered to handle "/test/" and the + %% requested path is "/test/foo/bar", the local path is + %% ["foo", "bar"] + LocalPath = lists:nthtail(length(HandlerPathPrefix), Request#request.path), + R = try + HandlerModule:socket_handoff( + LocalPath, Request, Socket, SockMod, Trail, HandlerOpts) + catch error:undef -> + HandlerModule:process(LocalPath, Request) + end, + ejabberd_hooks:run(http_request_debug, [{LocalPath, Request}]), + R; + false -> + process(HandlersLeft, Request, Socket, SockMod, Trail) end. -process_request(#state{request_method = Method, options = Options, - request_path = {abs_path, Path}, request_auth = Auth, - request_lang = Lang, request_handlers = RequestHandlers, - request_host = Host, request_port = Port, - request_tp = TP, request_headers = RequestHeaders, - sockmod = SockMod, - socket = Socket} = State) - when Method=:='GET' orelse Method=:='HEAD' orelse Method=:='DELETE' orelse Method=:='OPTIONS' -> - case (catch url_decode_q_split(Path)) of - {'EXIT', _} -> +extract_path_query(#state{request_method = Method, + request_path = {abs_path, Path}}) + when Method =:= 'GET' orelse + Method =:= 'HEAD' orelse + Method =:= 'DELETE' orelse Method =:= 'OPTIONS' -> + case catch url_decode_q_split(Path) of + {'EXIT', _} -> false; + {NPath, Query} -> + LPath = normalize_path([NPE + || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), + LQuery = case catch parse_urlencoded(Query) of + {'EXIT', _Reason} -> []; + LQ -> LQ + end, + {LPath, LQuery, <<"">>} + end; +extract_path_query(#state{request_method = Method, + request_path = {abs_path, Path}, + request_content_length = Len, + sockmod = _SockMod, + socket = _Socket} = State) + when (Method =:= 'POST' orelse Method =:= 'PUT') andalso + is_integer(Len) -> + Data = recv_data(State, Len), + ?DEBUG("client data: ~p~n", [Data]), + case catch url_decode_q_split(Path) of + {'EXIT', _} -> false; + {NPath, _Query} -> + LPath = normalize_path([NPE + || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), + LQuery = case catch parse_urlencoded(Data) of + {'EXIT', _Reason} -> []; + LQ -> LQ + end, + {LPath, LQuery, Data} + end; +extract_path_query(_State) -> + false. + +process_request(#state{request_method = Method, + request_auth = Auth, + request_lang = Lang, + sockmod = SockMod, + socket = Socket, + options = Options, + request_host = Host, + request_port = Port, + request_tp = TP, + request_headers = RequestHeaders, + request_handlers = RequestHandlers, + trail = Trail} = State) -> + case extract_path_query(State) of + false -> make_bad_request(State); - {NPath, Query} -> - LPath = normalize_path([NPE || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), - LQuery = case (catch parse_urlencoded(Query)) of - {'EXIT', _Reason} -> - []; - LQ -> - LQ - end, + {LPath, LQuery, Data} -> {ok, IPHere} = case SockMod of gen_tcp -> @@ -396,92 +434,36 @@ process_request(#state{request_method = Method, options = Options, end, XFF = proplists:get_value('X-Forwarded-For', RequestHeaders, []), IP = analyze_ip_xff(IPHere, XFF, Host), - Request = #request{method = Method, - path = LPath, - opts = Options, - q = LQuery, - auth = Auth, - lang = Lang, - host = Host, - port = Port, - tp = TP, - headers = RequestHeaders, - ip = IP}, - %% XXX bard: This previously passed control to - %% ejabberd_web:process_get, now passes it to a local - %% procedure (process) that handles dispatching based on - %% URL path prefix. - case process(RequestHandlers, Request) of - El when element(1, El) == xmlel -> - make_xhtml_output(State, 200, [], El); - {Status, Headers, El} when - element(1, El) == xmlel -> - make_xhtml_output(State, Status, Headers, El); - Output when is_list(Output) or is_binary(Output) -> - make_text_output(State, 200, [], Output); - {Status, Headers, Output} when is_list(Output) or is_binary(Output) -> - make_text_output(State, Status, Headers, Output) - end - end; -process_request(#state{request_method = Method, options = Options, - request_path = {abs_path, Path}, request_auth = Auth, - request_content_length = Len, request_lang = Lang, - sockmod = SockMod, socket = Socket, request_host = Host, - request_port = Port, request_tp = TP, - request_headers = RequestHeaders, - request_handlers = RequestHandlers} = - State) - when (Method =:= 'POST' orelse Method =:= 'PUT') andalso - is_integer(Len) -> - {ok, IPHere} = case SockMod of - gen_tcp -> inet:peername(Socket); - _ -> SockMod:peername(Socket) - end, - XFF = proplists:get_value('X-Forwarded-For', - RequestHeaders, []), - IP = analyze_ip_xff(IPHere, XFF, Host), - case SockMod of - gen_tcp -> inet:setopts(Socket, [{packet, 0}]); - _ -> ok - end, - Data = recv_data(State, Len), - ?DEBUG("client data: ~p~n", [Data]), - case (catch url_decode_q_split(Path)) of - {'EXIT', _} -> - make_bad_request(State); - {NPath, _Query} -> - LPath = normalize_path([NPE || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), - LQuery = case (catch parse_urlencoded(Data)) of - {'EXIT', _Reason} -> - []; - LQ -> - LQ - end, - Request = #request{method = Method, - path = LPath, - q = LQuery, + Request = #request{method = Method, + path = LPath, + q = LQuery, + auth = Auth, + data = Data, + lang = Lang, + host = Host, + port = Port, + tp = TP, opts = Options, - auth = Auth, - data = Data, - lang = Lang, - host = Host, - port = Port, - tp = TP, - headers = RequestHeaders, - ip = IP}, - case process(RequestHandlers, Request) of - El when element(1, El) == xmlel -> - make_xhtml_output(State, 200, [], El); - {Status, Headers, El} when - element(1, El) == xmlel -> - make_xhtml_output(State, Status, Headers, El); - Output when is_list(Output) or is_binary(Output) -> - make_text_output(State, 200, [], Output); - {Status, Headers, Output} when is_list(Output) or is_binary(Output) -> - make_text_output(State, Status, Headers, Output) + headers = RequestHeaders, + ip = IP}, + case process(RequestHandlers, Request, Socket, SockMod, Trail) of + El when is_record(El, xmlel) -> + make_xhtml_output(State, 200, [], El); + {Status, Headers, El} + when is_record(El, xmlel) -> + make_xhtml_output(State, Status, Headers, El); + Output when is_binary(Output) or is_list(Output) -> + make_text_output(State, 200, [], Output); + {Status, Headers, Output} + when is_binary(Output) or is_list(Output) -> + make_text_output(State, Status, Headers, Output); + {Status, Reason, Headers, Output} + when is_binary(Output) or is_list(Output) -> + make_text_output(State, Status, Reason, Headers, Output); + _ -> + none end - end; -process_request(State) -> make_bad_request(State). + end. make_bad_request(State) -> %% Support for X-Forwarded-From @@ -836,6 +818,26 @@ old_integer_to_hex(I) when I >= 16 -> N = trunc(I / 16), old_integer_to_hex(N) ++ old_integer_to_hex(I rem 16). +% The following code is mostly taken from yaws_ssl.erl + +toupper(C) when C >= $a andalso C =< $z -> C - 32; +toupper(C) -> C. + +tolower(C) when C >= $A andalso C =< $Z -> C + 32; +tolower(C) -> C. + +normalize_header_name(Name) -> + normalize_header_name(Name, [], true). + +normalize_header_name(<<"">>, Acc, _) -> + iolist_to_binary(Acc); +normalize_header_name(<<"-", Rest/binary>>, Acc, _) -> + normalize_header_name(Rest, [Acc, "-"], true); +normalize_header_name(<<C:8, Rest/binary>>, Acc, true) -> + normalize_header_name(Rest, [Acc, toupper(C)], false); +normalize_header_name(<<C:8, Rest/binary>>, Acc, false) -> + normalize_header_name(Rest, [Acc, tolower(C)], false). + normalize_path(Path) -> normalize_path(Path, []). @@ -856,7 +858,7 @@ transform_listen_option(web_admin, Opts) -> transform_listen_option(http_bind, Opts) -> [{http_bind, true}|Opts]; transform_listen_option(http_poll, Opts) -> - [{http_poll, true}|Opts]; + Opts; transform_listen_option({request_handlers, Hs}, Opts) -> Hs1 = lists:map( fun({PList, Mod}) when is_list(PList) -> diff --git a/src/ejabberd_http_poll.erl b/src/ejabberd_http_poll.erl deleted file mode 100644 index 174c78211..000000000 --- a/src/ejabberd_http_poll.erl +++ /dev/null @@ -1,425 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_http_poll.erl -%%% Author : Alexey Shchepin <alexey@process-one.net> -%%% Purpose : HTTP Polling support (XEP-0025) -%%% Created : 4 Mar 2004 by Alexey Shchepin <alexey@process-one.net> -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne -%%% -%%% This program is free software; you can redistribute it and/or -%%% modify it under the terms of the GNU General Public License as -%%% published by the Free Software Foundation; either version 2 of the -%%% License, or (at your option) any later version. -%%% -%%% This program is distributed in the hope that it will be useful, -%%% but WITHOUT ANY WARRANTY; without even the implied warranty of -%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -%%% General Public License for more details. -%%% -%%% You should have received a copy of the GNU General Public License along -%%% with this program; if not, write to the Free Software Foundation, Inc., -%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -%%% -%%%---------------------------------------------------------------------- - --module(ejabberd_http_poll). - --author('alexey@process-one.net'). - --behaviour(gen_fsm). - -%% External exports --export([start_link/3, init/1, handle_event/3, - handle_sync_event/4, code_change/4, handle_info/3, - terminate/3, send/2, setopts/2, sockname/1, peername/1, - controlling_process/2, close/1, process/2]). - --include("ejabberd.hrl"). --include("logger.hrl"). - --include("jlib.hrl"). - --include("ejabberd_http.hrl"). - --record(http_poll, {id :: pid() | binary(), pid :: pid()}). - --type poll_socket() :: #http_poll{}. --export_type([poll_socket/0]). - --record(state, - {id, key, socket, output = [], input = <<"">>, - waiting_input = false, last_receiver, http_poll_timeout, - timer}). - -%-define(DBGFSM, true). - --ifdef(DBGFSM). - --define(FSMOPTS, [{debug, [trace]}]). - --else. - --define(FSMOPTS, []). - --endif. - --define(HTTP_POLL_TIMEOUT, 300). - --define(CT, - {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). - --define(BAD_REQUEST, - [?CT, {<<"Set-Cookie">>, <<"ID=-3:0; expires=-1">>}]). - -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- -start(ID, Key, IP) -> - mnesia:create_table(http_poll, - [{ram_copies, [node()]}, - {attributes, record_info(fields, http_poll)}]), - supervisor:start_child(ejabberd_http_poll_sup, [ID, Key, IP]). - -start_link(ID, Key, IP) -> - gen_fsm:start_link(?MODULE, [ID, Key, IP], ?FSMOPTS). - -send({http_poll, FsmRef, _IP}, Packet) -> - gen_fsm:sync_send_all_state_event(FsmRef, - {send, Packet}). - -setopts({http_poll, FsmRef, _IP}, Opts) -> - case lists:member({active, once}, Opts) of - true -> - gen_fsm:send_all_state_event(FsmRef, - {activate, self()}); - _ -> ok - end. - -sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}. - -peername({http_poll, _FsmRef, IP}) -> {ok, IP}. - -controlling_process(_Socket, _Pid) -> ok. - -close({http_poll, FsmRef, _IP}) -> - catch gen_fsm:sync_send_all_state_event(FsmRef, close). - -process([], - #request{data = Data, ip = IP} = _Request) -> - case catch parse_request(Data) of - {ok, ID1, Key, NewKey, Packet} -> - ID = if - (ID1 == <<"0">>) or (ID1 == <<"mobile">>) -> - NewID = p1_sha:sha(term_to_binary({now(), make_ref()})), - {ok, Pid} = start(NewID, <<"">>, IP), - mnesia:transaction( - fun() -> - mnesia:write(#http_poll{id = NewID, pid = Pid}) - end), - NewID; - true -> - ID1 - end, - case http_put(ID, Key, NewKey, Packet) of - {error, not_exists} -> - {200, ?BAD_REQUEST, <<"">>}; - {error, bad_key} -> - {200, ?BAD_REQUEST, <<"">>}; - ok -> - receive - after 100 -> ok - end, - case http_get(ID) of - {error, not_exists} -> - {200, ?BAD_REQUEST, <<"">>}; - {ok, OutPacket} -> - if - ID == ID1 -> - Cookie = <<"ID=", ID/binary, "; expires=-1">>, - {200, [?CT, {<<"Set-Cookie">>, Cookie}], - OutPacket}; - ID1 == <<"mobile">> -> - {200, [?CT], [ID, $\n, OutPacket]}; - true -> - Cookie = <<"ID=", ID/binary, "; expires=-1">>, - {200, [?CT, {<<"Set-Cookie">>, Cookie}], - OutPacket} - end - end - end; - _ -> - HumanHTMLxmlel = get_human_html_xmlel(), - {200, [?CT, {<<"Set-Cookie">>, <<"ID=-2:0; expires=-1">>}], HumanHTMLxmlel} - end; -process(_, _Request) -> - {400, [], - #xmlel{name = <<"h1">>, attrs = [], - children = [{xmlcdata, <<"400 Bad Request">>}]}}. - -%% Code copied from mod_http_bind.erl and customized -get_human_html_xmlel() -> - Heading = <<"ejabberd ", - (iolist_to_binary(atom_to_list(?MODULE)))/binary>>, - #xmlel{name = <<"html">>, - attrs = - [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], - children = - [#xmlel{name = <<"head">>, attrs = [], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = [{xmlcdata, Heading}]}]}, - #xmlel{name = <<"body">>, attrs = [], - children = - [#xmlel{name = <<"h1">>, attrs = [], - children = [{xmlcdata, Heading}]}, - #xmlel{name = <<"p">>, attrs = [], - children = - [{xmlcdata, <<"An implementation of ">>}, - #xmlel{name = <<"a">>, - attrs = - [{<<"href">>, - <<"http://xmpp.org/extensions/xep-0025.html">>}], - children = - [{xmlcdata, - <<"Jabber HTTP Polling (XEP-0025)">>}]}]}, - #xmlel{name = <<"p">>, attrs = [], - children = - [{xmlcdata, - <<"This web page is only informative. To " - "use HTTP-Poll you need a Jabber/XMPP " - "client that supports it.">>}]}]}]}. - -%%%---------------------------------------------------------------------- -%%% Callback functions from gen_fsm -%%%---------------------------------------------------------------------- - -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- -init([ID, Key, IP]) -> - ?INFO_MSG("started: ~p", [{ID, Key, IP}]), - Opts = ejabberd_c2s_config:get_c2s_limits(), - HTTPPollTimeout = ejabberd_config:get_option( - {http_poll_timeout, ?MYNAME}, - fun(I) when is_integer(I), I>0 -> I end, - ?HTTP_POLL_TIMEOUT) * 1000, - Socket = {http_poll, self(), IP}, - ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket, - Opts), - Timer = erlang:start_timer(HTTPPollTimeout, self(), []), - {ok, loop, - #state{id = ID, key = Key, socket = Socket, - http_poll_timeout = HTTPPollTimeout, timer = Timer}}. - -%%---------------------------------------------------------------------- -%% Func: StateName/2 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- - -%%---------------------------------------------------------------------- -%% Func: StateName/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -%state_name(Event, From, StateData) -> -% Reply = ok, -% {reply, Reply, state_name, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -handle_event({activate, From}, StateName, StateData) -> - case StateData#state.input of - <<"">> -> - {next_state, StateName, - StateData#state{waiting_input = {From, ok}}}; - Input -> - Receiver = From, - Receiver ! - {tcp, StateData#state.socket, Input}, - {next_state, StateName, - StateData#state{input = <<"">>, waiting_input = false, - last_receiver = Receiver}} - end; -handle_event(_Event, StateName, StateData) -> - {next_state, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -handle_sync_event({send, Packet}, _From, StateName, - StateData) -> - Packet2 = iolist_to_binary(Packet), - Output = StateData#state.output ++ [Packet2], - Reply = ok, - {reply, Reply, StateName, - StateData#state{output = Output}}; -handle_sync_event(stop, _From, _StateName, StateData) -> - Reply = ok, {stop, normal, Reply, StateData}; -handle_sync_event({http_put, Key, NewKey, Packet}, - _From, StateName, StateData) -> - Allow = case StateData#state.key of - <<"">> -> true; - OldKey -> - NextKey = jlib:encode_base64((p1_sha:sha1(Key))), - if OldKey == NextKey -> true; - true -> false - end - end, - if Allow -> - case StateData#state.waiting_input of - false -> - Input = <<(StateData#state.input)/binary, Packet/binary>>, - Reply = ok, - {reply, Reply, StateName, - StateData#state{input = Input, key = NewKey}}; - {Receiver, _Tag} -> - Receiver ! - {tcp, StateData#state.socket, iolist_to_binary(Packet)}, - cancel_timer(StateData#state.timer), - Timer = - erlang:start_timer(StateData#state.http_poll_timeout, - self(), []), - Reply = ok, - {reply, Reply, StateName, - StateData#state{waiting_input = false, - last_receiver = Receiver, key = NewKey, - timer = Timer}} - end; - true -> - Reply = {error, bad_key}, - {reply, Reply, StateName, StateData} - end; -handle_sync_event(http_get, _From, StateName, - StateData) -> - Reply = {ok, StateData#state.output}, - {reply, Reply, StateName, - StateData#state{output = []}}; -handle_sync_event(_Event, _From, StateName, - StateData) -> - Reply = ok, {reply, Reply, StateName, StateData}. - -code_change(_OldVsn, StateName, StateData, _Extra) -> - {ok, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -handle_info({timeout, Timer, _}, _StateName, - #state{timer = Timer} = StateData) -> - {stop, normal, StateData}; -handle_info(_, StateName, StateData) -> - {next_state, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: terminate/3 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- -terminate(_Reason, _StateName, StateData) -> - mnesia:transaction( - fun() -> - mnesia:delete({http_poll, StateData#state.id}) - end), - case StateData#state.waiting_input of - false -> - case StateData#state.last_receiver of - undefined -> ok; - Receiver -> - Receiver ! {tcp_closed, StateData#state.socket} - end; - {Receiver, _Tag} -> - Receiver ! {tcp_closed, StateData#state.socket} - end, - catch resend_messages(StateData#state.output), - ok. - -%%%---------------------------------------------------------------------- -%%% Internal functions -%%%---------------------------------------------------------------------- - -http_put(ID, Key, NewKey, Packet) -> - case mnesia:dirty_read({http_poll, ID}) of - [] -> - {error, not_exists}; - [#http_poll{pid = FsmRef}] -> - gen_fsm:sync_send_all_state_event( - FsmRef, {http_put, Key, NewKey, Packet}) - end. - -http_get(ID) -> - case mnesia:dirty_read({http_poll, ID}) of - [] -> - {error, not_exists}; - [#http_poll{pid = FsmRef}] -> - gen_fsm:sync_send_all_state_event(FsmRef, http_get) - end. - -parse_request(Data) -> - Comma = str:chr(Data, $,), - Header = str:substr(Data, 1, Comma - 1), - Packet = str:substr(Data, Comma + 1, byte_size(Data)), - {ID, Key, NewKey} = case str:tokens(Header, <<";">>) of - [ID1] -> {ID1, <<"">>, <<"">>}; - [ID1, Key1] -> {ID1, Key1, Key1}; - [ID1, Key1, NewKey1] -> {ID1, Key1, NewKey1} - end, - {ok, ID, Key, NewKey, Packet}. - -cancel_timer(Timer) -> - erlang:cancel_timer(Timer), - receive {timeout, Timer, _} -> ok after 0 -> ok end. - -%% Resend the polled messages -resend_messages(Messages) -> -%% This function is used to resend messages that have been polled but not -%% delivered. - lists:foreach(fun (Packet) -> resend_message(Packet) - end, - Messages). - -resend_message(Packet) -> - #xmlel{name = Name} = ParsedPacket = - xml_stream:parse_element(Packet), - if Name == <<"iq">>; - Name == <<"message">>; - Name == <<"presence">> -> - From = get_jid(<<"from">>, ParsedPacket), - To = get_jid(<<"to">>, ParsedPacket), - ?DEBUG("Resend ~p ~p ~p~n", [From, To, ParsedPacket]), - ejabberd_router:route(From, To, ParsedPacket); - true -> ok - end. - -%% Type can be "from" or "to" -%% Parsed packet is a parsed Jabber packet. -get_jid(Type, ParsedPacket) -> - case xml:get_tag_attr(Type, ParsedPacket) of - {value, StringJid} -> jlib:string_to_jid(StringJid); - false -> jlib:make_jid(<<"">>, <<"">>, <<"">>) - end. diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl new file mode 100644 index 000000000..a0cc31e2a --- /dev/null +++ b/src/ejabberd_http_ws.erl @@ -0,0 +1,355 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_websocket.erl +%%% Author : Eric Cestari <ecestari@process-one.net> +%%% Purpose : XMPP Websocket support +%%% Created : 09-10-2010 by Eric Cestari <ecestari@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- +-module(ejabberd_http_ws). + +-author('ecestari@process-one.net'). + +-behaviour(gen_fsm). + +% External exports +-export([start/1, start_link/1, init/1, handle_event/3, + handle_sync_event/4, code_change/4, handle_info/3, + terminate/3, send_xml/2, setopts/2, sockname/1, peername/1, + controlling_process/2, become_controller/2, close/1, + socket_handoff/6]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). + +-include("jlib.hrl"). + +-include("ejabberd_http.hrl"). + +-define(PING_INTERVAL, 60). +-define(WEBSOCKET_TIMEOUT, 300). + +-record(state, + {socket :: ws_socket(), + ping_interval = ?PING_INTERVAL :: non_neg_integer(), + ping_timer = make_ref() :: reference(), + pong_expected :: boolean(), + timeout = ?WEBSOCKET_TIMEOUT :: non_neg_integer(), + timer = make_ref() :: reference(), + input = [] :: list(), + waiting_input = false :: false | pid(), + last_receiver :: pid(), + ws :: {#ws{}, pid()}, + rfc_compilant = undefined :: boolean() | undefined}). + +%-define(DBGFSM, true). + +-ifdef(DBGFSM). + +-define(FSMOPTS, [{debug, [trace]}]). + +-else. + +-define(FSMOPTS, []). + +-endif. + +-type ws_socket() :: {http_ws, pid(), {inet:ip_address(), inet:port_number()}}. +-export_type([ws_socket/0]). + +start(WS) -> + supervisor:start_child(ejabberd_wsloop_sup, [WS]). + +start_link(WS) -> + gen_fsm:start_link(?MODULE, [WS], ?FSMOPTS). + +send_xml({http_ws, FsmRef, _IP}, Packet) -> + gen_fsm:sync_send_all_state_event(FsmRef, + {send_xml, Packet}). + +setopts({http_ws, FsmRef, _IP}, Opts) -> + case lists:member({active, once}, Opts) of + true -> + gen_fsm:send_all_state_event(FsmRef, + {activate, self()}); + _ -> ok + end. + +sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}. + +peername({http_ws, _FsmRef, IP}) -> {ok, IP}. + +controlling_process(_Socket, _Pid) -> ok. + +become_controller(FsmRef, C2SPid) -> + gen_fsm:send_all_state_event(FsmRef, + {become_controller, C2SPid}). + +close({http_ws, FsmRef, _IP}) -> + catch gen_fsm:sync_send_all_state_event(FsmRef, close). + +socket_handoff(LocalPath, Request, Socket, SockMod, Buf, Opts) -> + ejabberd_websocket:socket_handoff(LocalPath, Request, Socket, SockMod, + Buf, Opts, ?MODULE, fun get_human_html_xmlel/0). + +%%% Internal + +init([{#ws{ip = IP}, _} = WS]) -> + Opts = [{xml_socket, true} | ejabberd_c2s_config:get_c2s_limits()], + PingInterval = ejabberd_config:get_option( + {websocket_ping_interval, ?MYNAME}, + fun(I) when is_integer(I), I>=0 -> I end, + ?PING_INTERVAL) * 1000, + WSTimeout = ejabberd_config:get_option( + {websocket_timeout, ?MYNAME}, + fun(I) when is_integer(I), I>0 -> I end, + ?WEBSOCKET_TIMEOUT) * 1000, + Socket = {http_ws, self(), IP}, + ?DEBUG("Client connected through websocket ~p", + [Socket]), + ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket, + Opts), + Timer = erlang:start_timer(WSTimeout, self(), []), + {ok, loop, + #state{socket = Socket, timeout = WSTimeout, + timer = Timer, ws = WS, + ping_interval = PingInterval}}. + +handle_event({activate, From}, StateName, StateData) -> + case StateData#state.input of + [] -> + {next_state, StateName, + StateData#state{waiting_input = From}}; + Input -> + Receiver = From, + lists:foreach(fun(I) when is_binary(I)-> + Receiver ! {tcp, StateData#state.socket, I}; + (I2) -> + Receiver ! {tcp, StateData#state.socket, [I2]} + end, Input), + {next_state, StateName, + StateData#state{input = [], waiting_input = false, + last_receiver = Receiver}} + end. + +handle_sync_event({send_xml, Packet}, _From, StateName, + #state{ws = {_, WsPid}, rfc_compilant = R} = StateData) -> + Packet2 = case {case R of undefined -> true; V -> V end, Packet} of + {true, {xmlstreamstart, _, Attrs}} -> + Attrs2 = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>} | + lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))], + {xmlstreamelement, #xmlel{name = <<"open">>, attrs = Attrs2}}; + {true, {xmlstreamend, _}} -> + {xmlstreamelement, #xmlel{name = <<"close">>, + attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]}}; + {true, {xmlstreamraw, <<"\r\n\r\n">>}} -> % cdata ping + skip; + {true, {xmlstreamelement, #xmlel{name=Name2} = El2}} -> + El3 = case Name2 of + <<"stream:", _/binary>> -> + xml:replace_tag_attr(<<"xmlns:stream">>, ?NS_STREAM, El2); + _ -> + case xml:get_tag_attr_s(<<"xmlns">>, El2) of + <<"">> -> + xml:replace_tag_attr(<<"xmlns">>, <<"jabber:client">>, El2); + _ -> + El2 + end + end, + {xmlstreamelement , El3}; + _ -> + Packet + end, + case Packet2 of + {xmlstreamstart, Name, Attrs3} -> + B = xml:element_to_binary(#xmlel{name = Name, attrs = Attrs3}), + WsPid ! {send, <<(binary:part(B, 0, byte_size(B)-2))/binary, ">">>}; + {xmlstreamend, Name} -> + WsPid ! {send, <<"</", Name/binary, ">">>}; + {xmlstreamelement, El} -> + WsPid ! {send, xml:element_to_binary(El)}; + {xmlstreamraw, Bin} -> + WsPid ! {send, Bin}; + {xmlstreamcdata, Bin2} -> + WsPid ! {send, Bin2}; + skip -> + ok + end, + SN2 = case Packet2 of + {xmlstreamelement, #xmlel{name = <<"close">>}} -> + stream_end_sent; + _ -> + StateName + end, + {reply, ok, SN2, StateData}; +handle_sync_event(close, _From, StateName, #state{ws = {_, WsPid}, rfc_compilant = true} = StateData) + when StateName /= stream_end_sent -> + Close = #xmlel{name = <<"close">>, + attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]}, + WsPid ! {send, xml:element_to_binary(Close)}, + {stop, normal, StateData}; +handle_sync_event(close, _From, _StateName, StateData) -> + {stop, normal, StateData}. + +handle_info(closed, _StateName, StateData) -> + {stop, normal, StateData}; +handle_info({received, Packet}, StateName, StateDataI) -> + {StateData, Parsed} = parse(StateDataI, Packet), + SD = case StateData#state.waiting_input of + false -> + Input = StateData#state.input ++ if is_binary(Parsed) -> [Parsed]; true -> Parsed end, + StateData#state{input = Input}; + Receiver -> + Receiver ! {tcp, StateData#state.socket, Parsed}, + setup_timers(StateData#state{waiting_input = false, + last_receiver = Receiver}) + end, + {next_state, StateName, SD}; +handle_info(PingPong, StateName, StateData) when PingPong == ping orelse + PingPong == pong -> + StateData2 = setup_timers(StateData), + {next_state, StateName, + StateData2#state{pong_expected = false}}; +handle_info({timeout, Timer, _}, _StateName, + #state{timer = Timer} = StateData) -> + {stop, normal, StateData}; +handle_info({timeout, Timer, _}, StateName, + #state{ping_timer = Timer, ws = {_, WsPid}} = StateData) -> + case StateData#state.pong_expected of + false -> + cancel_timer(StateData#state.ping_timer), + PingTimer = erlang:start_timer(StateData#state.ping_interval, + self(), []), + WsPid ! {ping, <<>>}, + {next_state, StateName, + StateData#state{ping_timer = PingTimer, pong_expected = true}}; + true -> + {stop, normal, StateData} + end; +handle_info(_, StateName, StateData) -> + {next_state, StateName, StateData}. + +code_change(_OldVsn, StateName, StateData, _Extra) -> + {ok, StateName, StateData}. + +terminate(_Reason, _StateName, StateData) -> + case StateData#state.waiting_input of + false -> ok; + Receiver -> + ?DEBUG("C2S Pid : ~p", [Receiver]), + Receiver ! {tcp_closed, StateData#state.socket} + end, + ok. + +setup_timers(StateData) -> + cancel_timer(StateData#state.timer), + Timer = erlang:start_timer(StateData#state.timeout, + self(), []), + cancel_timer(StateData#state.ping_timer), + PingTimer = case {StateData#state.ping_interval, StateData#state.rfc_compilant} of + {0, _} -> StateData#state.ping_timer; + {_, false} -> StateData#state.ping_timer; + {V, _} -> erlang:start_timer(V, self(), []) + end, + StateData#state{timer = Timer, ping_timer = PingTimer, + pong_expected = false}. + +cancel_timer(Timer) -> + erlang:cancel_timer(Timer), + receive {timeout, Timer, _} -> ok after 0 -> ok end. + +get_human_html_xmlel() -> + Heading = <<"ejabberd ", (jlib:atom_to_binary(?MODULE))/binary>>, + #xmlel{name = <<"html">>, + attrs = + [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], + children = + [#xmlel{name = <<"head">>, attrs = [], + children = + [#xmlel{name = <<"title">>, attrs = [], + children = [{xmlcdata, Heading}]}]}, + #xmlel{name = <<"body">>, attrs = [], + children = + [#xmlel{name = <<"h1">>, attrs = [], + children = [{xmlcdata, Heading}]}, + #xmlel{name = <<"p">>, attrs = [], + children = + [{xmlcdata, <<"An implementation of ">>}, + #xmlel{name = <<"a">>, + attrs = + [{<<"href">>, + <<"http://tools.ietf.org/html/rfc6455">>}], + children = + [{xmlcdata, + <<"WebSocket protocol">>}]}]}, + #xmlel{name = <<"p">>, attrs = [], + children = + [{xmlcdata, + <<"This web page is only informative. To " + "use WebSocket connection you need a Jabber/XMPP " + "client that supports it.">>}]}]}]}. + + +parse(#state{rfc_compilant = C} = State, Data) -> + case C of + undefined -> + P = xml_stream:new(self()), + P2 = xml_stream:parse(P, Data), + xml_stream:close(P2), + case parsed_items([]) of + error -> + {State#state{rfc_compilant = true}, <<"parse error">>}; + [] -> + {State#state{rfc_compilant = true}, <<"parse error">>}; + [{xmlstreamstart, <<"open">>, _} | _] -> + parse(State#state{rfc_compilant = true}, Data); + _ -> + parse(State#state{rfc_compilant = false}, Data) + end; + true -> + El = xml_stream:parse_element(Data), + case El of + #xmlel{name = <<"open">>, attrs = Attrs} -> + Attrs2 = [{<<"xmlns:stream">>, ?NS_STREAM}, {<<"xmlns">>, <<"jabber:client">>} | + lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))], + {State, [{xmlstreamstart, <<"stream:stream">>, Attrs2}]}; + #xmlel{name = <<"close">>} -> + {State, [{xmlstreamend, <<"stream:stream">>}]}; + {error, _} -> + {State, <<"parse error">>}; + _ -> + {State, [El]} + end; + false -> + {State, Data} + end. + +parsed_items(List) -> + receive + {'$gen_event', El} + when element(1, El) == xmlel; + element(1, El) == xmlstreamstart; + element(1, El) == xmlstreamelement; + element(1, El) == xmlstreamend -> + parsed_items([El | List]); + {'$gen_event', {xmlstreamerror, _}} -> + error + after 0 -> + lists:reverse(List) + end. diff --git a/src/ejabberd_listener.erl b/src/ejabberd_listener.erl index 7db5ab826..0cfca0aa0 100644 --- a/src/ejabberd_listener.erl +++ b/src/ejabberd_listener.erl @@ -195,20 +195,15 @@ listen_tcp(PortIP, Module, SockOpts, Port, IPS) -> ets:delete(listen_sockets, Port), ListenSocket; _ -> - SockOpts2 = try erlang:system_info(otp_release) >= "R13B" of - true -> [{send_timeout_close, true} | SockOpts]; - false -> SockOpts - catch - _:_ -> [] - end, Res = gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}, {reuseaddr, true}, {nodelay, true}, {send_timeout, ?TCP_SEND_TIMEOUT}, + {send_timeout_close, true}, {keepalive, true} | - SockOpts2]), + SockOpts]), case Res of {ok, ListenSocket} -> ListenSocket; @@ -546,7 +541,7 @@ normalize_proto(UnknownProto) -> socket_error(Reason, PortIP, Module, SockOpts, Port, IPS) -> ReasonT = case Reason of eaddrnotavail -> - "IP address not available: " ++ IPS; + "IP address not available: " ++ binary_to_list(IPS); eaddrinuse -> "IP address and port number already used: " ++binary_to_list(IPS)++" "++integer_to_list(Port); diff --git a/src/ejabberd_logger.erl b/src/ejabberd_logger.erl index 59beca16d..a00ac9942 100644 --- a/src/ejabberd_logger.erl +++ b/src/ejabberd_logger.erl @@ -46,8 +46,10 @@ %% If not defined it checks the environment variable EJABBERD_LOG_PATH. %% And if that one is neither defined, returns the default value: %% "ejabberd.log" in current directory. +%% Note: If the directory where to place the ejabberd log file to not exist, +%% it is not created and no log file will be generated. get_log_path() -> - case application:get_env(ejabberd, log_path) of + case ejabberd_config:env_binary_to_list(ejabberd, log_path) of {ok, Path} -> Path; undefined -> diff --git a/src/ejabberd_odbc.erl b/src/ejabberd_odbc.erl index 9cf30f53e..09f17a635 100644 --- a/src/ejabberd_odbc.erl +++ b/src/ejabberd_odbc.erl @@ -40,6 +40,8 @@ escape/1, escape_like/1, to_bool/1, + sqlite_db/1, + sqlite_file/1, encode_term/1, decode_term/1, keep_alive/1]). @@ -58,11 +60,11 @@ -record(state, {db_ref = self() :: pid(), - db_type = odbc :: pgsql | mysql | odbc, + db_type = odbc :: pgsql | mysql | sqlite | odbc, start_interval = 0 :: non_neg_integer(), host = <<"">> :: binary(), max_pending_requests_len :: non_neg_integer(), - pending_requests = {0, queue:new()} :: {non_neg_integer(), queue()}}). + pending_requests = {0, queue:new()} :: {non_neg_integer(), ?TQUEUE}}). -define(STATE_KEY, ejabberd_odbc_state). @@ -199,6 +201,22 @@ decode_term(Bin) -> {ok, Term} = erl_parse:parse_term(Tokens), Term. +-spec sqlite_db(binary()) -> atom(). +sqlite_db(Host) -> + list_to_atom("ejabberd_sqlite_" ++ binary_to_list(Host)). + +-spec sqlite_file(binary()) -> string(). +sqlite_file(Host) -> + case ejabberd_config:get_option({odbc_database, Host}, + fun iolist_to_binary/1) of + undefined -> + {ok, Cwd} = file:get_cwd(), + filename:join([Cwd, "sqlite", atom_to_list(node()), + binary_to_list(Host), "ejabberd.db"]); + File -> + binary_to_list(File) + end. + %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- @@ -224,7 +242,8 @@ init([Host, StartInterval]) -> connecting(connect, #state{host = Host} = State) -> ConnectRes = case db_opts(Host) of [mysql | Args] -> apply(fun mysql_connect/5, Args); - [pgsql | Args] -> apply(fun pgsql_connect/5, Args); + [pgsql | Args] -> apply(fun pgsql_connect/5, Args); + [sqlite | Args] -> apply(fun sqlite_connect/1, Args); [odbc | Args] -> apply(fun odbc_connect/1, Args) end, {_, PendingRequests} = State#state.pending_requests, @@ -327,8 +346,9 @@ handle_info(Info, StateName, State) -> terminate(_Reason, _StateName, State) -> ejabberd_odbc_sup:remove_pid(State#state.host, self()), case State#state.db_type of - mysql -> catch p1_mysql_conn:stop(State#state.db_ref); - _ -> ok + mysql -> catch p1_mysql_conn:stop(State#state.db_ref); + sqlite -> catch sqlite3:close(sqlite_db(State#state.host)); + _ -> ok end, ok. @@ -456,7 +476,10 @@ sql_query_internal(Query) -> [{timeout, (?TRANSACTION_TIMEOUT) - 1000}, {result_type, binary}])), %% ?INFO_MSG("MySQL, Received result~n~p~n", [R]), - R + R; + sqlite -> + Host = State#state.host, + sqlite_to_odbc(Host, sqlite3:sql_exec(sqlite_db(Host), Query)) end, case Res of {error, <<"No SQL-driver information available.">>} -> @@ -488,6 +511,47 @@ odbc_connect(SQLServer) -> ejabberd:start_app(odbc), odbc:connect(binary_to_list(SQLServer), [{scrollable_cursors, off}]). +%% == Native SQLite code + +%% part of init/1 +%% Open a database connection to SQLite + +sqlite_connect(Host) -> + File = sqlite_file(Host), + case filelib:ensure_dir(File) of + ok -> + case sqlite3:open(sqlite_db(Host), [{file, File}]) of + {ok, Ref} -> + sqlite3:sql_exec( + sqlite_db(Host), "pragma foreign_keys = on"), + {ok, Ref}; + {error, {already_started, Ref}} -> + {ok, Ref}; + {error, Reason} -> + {error, Reason} + end; + Err -> + Err + end. + +%% Convert SQLite query result to Erlang ODBC result formalism +sqlite_to_odbc(Host, ok) -> + {updated, sqlite3:changes(sqlite_db(Host))}; +sqlite_to_odbc(Host, {rowid, _}) -> + {updated, sqlite3:changes(sqlite_db(Host))}; +sqlite_to_odbc(_Host, [{columns, Columns}, {rows, TRows}]) -> + Rows = [lists:map( + fun(I) when is_integer(I) -> + jlib:integer_to_binary(I); + (B) -> + B + end, tuple_to_list(Row)) || Row <- TRows], + {selected, [list_to_binary(C) || C <- Columns], Rows}; +sqlite_to_odbc(_Host, {error, _Code, Reason}) -> + {error, Reason}; +sqlite_to_odbc(_Host, _) -> + {updated, undefined}. + %% == Native PostgreSQL code %% part of init/1 @@ -502,6 +566,7 @@ pgsql_connect(Server, Port, DB, Username, Password) -> {ok, Ref} -> pgsql:squery(Ref, [<<"alter database ">>, DB, <<" set ">>, <<"standard_conforming_strings='off';">>]), + pgsql:squery(Ref, [<<"set standard_conforming_strings to 'off';">>]), {ok, Ref}; Err -> Err @@ -584,6 +649,7 @@ db_opts(Host) -> Type = ejabberd_config:get_option({odbc_type, Host}, fun(mysql) -> mysql; (pgsql) -> pgsql; + (sqlite) -> sqlite; (odbc) -> odbc end, odbc), Server = ejabberd_config:get_option({odbc_server, Host}, @@ -592,6 +658,8 @@ db_opts(Host) -> case Type of odbc -> [odbc, Server]; + sqlite -> + [sqlite, Host]; _ -> Port = ejabberd_config:get_option( {odbc_port, Host}, diff --git a/src/ejabberd_odbc_sup.erl b/src/ejabberd_odbc_sup.erl index 602e7e03b..37128e265 100644 --- a/src/ejabberd_odbc_sup.erl +++ b/src/ejabberd_odbc_sup.erl @@ -67,6 +67,19 @@ init([Host]) -> {odbc_start_interval, Host}, fun(I) when is_integer(I), I>0 -> I end, ?DEFAULT_ODBC_START_INTERVAL), + Type = ejabberd_config:get_option({odbc_type, Host}, + fun(mysql) -> mysql; + (pgsql) -> pgsql; + (sqlite) -> sqlite; + (odbc) -> odbc + end, odbc), + case Type of + sqlite -> + check_sqlite_db(Host); + _ -> + ok + end, + {ok, {{one_for_one, PoolSize * 10, 1}, lists:map(fun (I) -> @@ -113,5 +126,82 @@ transform_options({odbc_server, {mysql, Server, DB, User, Pass}}, Opts) -> transform_options({odbc_server, {mysql, Server, ?MYSQL_PORT, DB, User, Pass}}, Opts); transform_options({odbc_server, {pgsql, Server, DB, User, Pass}}, Opts) -> transform_options({odbc_server, {pgsql, Server, ?PGSQL_PORT, DB, User, Pass}}, Opts); +transform_options({odbc_server, {sqlite, DB}}, Opts) -> + transform_options({odbc_server, {sqlite, DB}}, Opts); transform_options(Opt, Opts) -> [Opt|Opts]. + +check_sqlite_db(Host) -> + DB = ejabberd_odbc:sqlite_db(Host), + File = ejabberd_odbc:sqlite_file(Host), + Ret = case filelib:ensure_dir(File) of + ok -> + case sqlite3:open(DB, [{file, File}]) of + {ok, _Ref} -> ok; + {error, {already_started, _Ref}} -> ok; + {error, R} -> {error, R} + end; + Err -> + Err + end, + case Ret of + ok -> + sqlite3:sql_exec(DB, "pragma foreign_keys = on"), + case sqlite3:list_tables(DB) of + [] -> + create_sqlite_tables(DB), + sqlite3:close(DB), + ok; + [_H | _] -> + ok + end; + {error, Reason} -> + ?INFO_MSG("Failed open sqlite database, reason ~p", [Reason]) + end. + +create_sqlite_tables(DB) -> + SqlDir = case code:priv_dir(ejabberd) of + {error, _} -> + ?SQL_DIR; + PrivDir -> + filename:join(PrivDir, "sql") + end, + File = filename:join(SqlDir, "lite.sql"), + case file:open(File, [read, binary]) of + {ok, Fd} -> + Qs = read_lines(Fd, File, []), + ok = sqlite3:sql_exec(DB, "begin"), + [ok = sqlite3:sql_exec(DB, Q) || Q <- Qs], + ok = sqlite3:sql_exec(DB, "commit"); + {error, Reason} -> + ?INFO_MSG("Failed to read SQLite schema file: ~s", + [file:format_error(Reason)]) + end. + +read_lines(Fd, File, Acc) -> + case file:read_line(Fd) of + {ok, Line} -> + NewAcc = case str:strip(str:strip(Line, both, $\r), both, $\n) of + <<"--", _/binary>> -> + Acc; + <<>> -> + Acc; + _ -> + [Line|Acc] + end, + read_lines(Fd, File, NewAcc); + eof -> + QueryList = str:tokens(list_to_binary(lists:reverse(Acc)), <<";">>), + lists:flatmap( + fun(Query) -> + case str:strip(str:strip(Query, both, $\r), both, $\n) of + <<>> -> + []; + Q -> + [<<Q/binary, $;>>] + end + end, QueryList); + {error, _} = Err -> + ?ERROR_MSG("Failed read from lite.sql, reason: ~p", [Err]), + [] + end. diff --git a/src/ejabberd_rdbms.erl b/src/ejabberd_rdbms.erl index e71728da5..93bd9fc49 100644 --- a/src/ejabberd_rdbms.erl +++ b/src/ejabberd_rdbms.erl @@ -74,10 +74,12 @@ needs_odbc(Host) -> case ejabberd_config:get_option({odbc_type, LHost}, fun(mysql) -> mysql; (pgsql) -> pgsql; + (sqlite) -> sqlite; (odbc) -> odbc end, undefined) of mysql -> {true, p1_mysql}; pgsql -> {true, p1_pgsql}; + sqlite -> {true, sqlite3}; odbc -> {true, odbc}; undefined -> false end. diff --git a/src/ejabberd_receiver.erl b/src/ejabberd_receiver.erl index 819e6d898..f63ae1ccb 100644 --- a/src/ejabberd_receiver.erl +++ b/src/ejabberd_receiver.erl @@ -243,7 +243,13 @@ handle_info({Tag, _TCPSocket, Data}, {ok, TLSData} -> {noreply, process_data(TLSData, State), ?HIBERNATE_TIMEOUT}; - {error, _Reason} -> {stop, normal, State} + {error, Reason} -> + if is_binary(Reason) -> + ?ERROR_MSG("TLS error = ~s", [Reason]); + true -> + ok + end, + {stop, normal, State} end; ezlib -> case ezlib:recv_data(Socket, Data) of diff --git a/src/ejabberd_riak.erl b/src/ejabberd_riak.erl index f677ca91a..c8084674f 100644 --- a/src/ejabberd_riak.erl +++ b/src/ejabberd_riak.erl @@ -16,10 +16,9 @@ %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% -%%% You should have received a copy of the GNU General Public License -%%% along with this program; if not, write to the Free Software -%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA -%%% 02111-1307 USA +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_riak). diff --git a/src/ejabberd_riak_sup.erl b/src/ejabberd_riak_sup.erl index 9711e6652..bb36eb44d 100644 --- a/src/ejabberd_riak_sup.erl +++ b/src/ejabberd_riak_sup.erl @@ -17,10 +17,9 @@ %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% -%%% You should have received a copy of the GNU General Public License -%%% along with this program; if not, write to the Free Software -%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA -%%% 02111-1307 USA +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- @@ -76,7 +75,7 @@ is_riak_configured(Host) -> fun(L) when is_list(L) -> L end, []), ModuleWithRiakDBConfigured = lists:any( fun({_Module, Opts}) -> - gen_mod:db_type(Opts) == riak + gen_mod:db_type(Host, Opts) == riak end, Modules), ServerConfigured or PortConfigured or AuthConfigured or ModuleWithRiakDBConfigured. diff --git a/src/ejabberd_router_multicast.erl b/src/ejabberd_router_multicast.erl new file mode 100644 index 000000000..9cd7dd3dd --- /dev/null +++ b/src/ejabberd_router_multicast.erl @@ -0,0 +1,215 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_router_multicast.erl +%%% Author : Badlop <badlop@process-one.net> +%%% Purpose : Multicast router +%%% Created : 11 Aug 2007 by Badlop <badlop@process-one.net> +%%%---------------------------------------------------------------------- + +-module(ejabberd_router_multicast). +-author('alexey@process-one.net'). +-author('badlop@process-one.net'). + +-behaviour(gen_server). + +%% API +-export([route_multicast/4, + register_route/1, + unregister_route/1 + ]). + +-export([start_link/0]). + +%% 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("jlib.hrl"). + +-record(route_multicast, {domain, pid}). +-record(state, {}). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + + +route_multicast(From, Domain, Destinations, Packet) -> + case catch do_route(From, Domain, Destinations, Packet) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p~nwhen processing: ~p", + [Reason, {From, Domain, Destinations, Packet}]); + _ -> + ok + end. + +register_route(Domain) -> + case jlib:nameprep(Domain) of + error -> + erlang:error({invalid_domain, Domain}); + LDomain -> + Pid = self(), + F = fun() -> + mnesia:write(#route_multicast{domain = LDomain, + pid = Pid}) + end, + mnesia:transaction(F) + end. + +unregister_route(Domain) -> + case jlib:nameprep(Domain) of + error -> + erlang:error({invalid_domain, Domain}); + LDomain -> + Pid = self(), + F = fun() -> + case mnesia:select(route_multicast, + [{#route_multicast{pid = Pid, domain = LDomain, _ = '_'}, + [], + ['$_']}]) of + [R] -> mnesia:delete_object(R); + _ -> ok + end + end, + mnesia:transaction(F) + end. + + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([]) -> + mnesia:create_table(route_multicast, + [{ram_copies, [node()]}, + {type, bag}, + {attributes, + record_info(fields, route_multicast)}]), + mnesia:add_table_copy(route_multicast, node(), ram_copies), + mnesia:subscribe({table, route_multicast, simple}), + lists:foreach( + fun(Pid) -> + erlang:monitor(process, Pid) + end, + mnesia:dirty_select(route_multicast, [{{route_multicast, '_', '$1'}, [], ['$1']}])), + {ok, #state{}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info({route_multicast, From, Domain, Destinations, Packet}, State) -> + case catch do_route(From, Domain, Destinations, Packet) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p~nwhen processing: ~p", + [Reason, {From, Domain, Destinations, Packet}]); + _ -> + ok + end, + {noreply, State}; +handle_info({mnesia_table_event, {write, #route_multicast{pid = Pid}, _ActivityId}}, + State) -> + erlang:monitor(process, Pid), + {noreply, State}; +handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) -> + F = fun() -> + Es = mnesia:select( + route_multicast, + [{#route_multicast{pid = Pid, _ = '_'}, + [], + ['$_']}]), + lists:foreach( + fun(E) -> + mnesia:delete_object(E) + end, Es) + end, + mnesia:transaction(F), + {noreply, State}; +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +%% From = #jid +%% Destinations = [#jid] +do_route(From, Domain, Destinations, Packet) -> + + ?DEBUG("route_multicast~n\tfrom ~s~n\tdomain ~s~n\tdestinations ~p~n\tpacket ~p~n", + [jlib:jid_to_string(From), + Domain, + [jlib:jid_to_string(To) || To <- Destinations], + Packet]), + + %% Try to find an appropriate multicast service + case mnesia:dirty_read(route_multicast, Domain) of + + %% If no multicast service is available in this server, send manually + [] -> do_route_normal(From, Destinations, Packet); + + %% If available, send the packet using multicast service + [R] -> + case R#route_multicast.pid of + Pid when is_pid(Pid) -> + Pid ! {route_trusted, From, Destinations, Packet}; + _ -> do_route_normal(From, Destinations, Packet) + end + end. + +do_route_normal(From, Destinations, Packet) -> + [ejabberd_router:route(From, To, Packet) || To <- Destinations]. diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl index 7afac4715..1b40f03c2 100644 --- a/src/ejabberd_s2s_in.erl +++ b/src/ejabberd_s2s_in.erl @@ -58,7 +58,7 @@ server = <<"">> :: binary(), authenticated = false :: boolean(), auth_domain = <<"">> :: binary(), - connections = (?DICT):new() :: dict(), + connections = (?DICT):new() :: ?TDICT, timer = make_ref() :: reference()}). %-define(DBGFSM, true). diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index 3445023ed..97164326d 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -77,7 +77,7 @@ try_auth = true :: boolean(), myname = <<"">> :: binary(), server = <<"">> :: binary(), - queue = queue:new() :: queue(), + queue = queue:new() :: ?TQUEUE, delay_to_retry = undefined_delay :: undefined_delay | non_neg_integer(), new = false :: false | binary(), verify = false :: false | {pid(), binary(), binary()}, diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index a0eb710de..678452951 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -30,7 +30,8 @@ -behaviour(gen_server). %% API --export([start_link/0, +-export([start/0, + start_link/0, route/3, open_session/5, open_session/6, @@ -73,11 +74,18 @@ -include("jlib.hrl"). -include("ejabberd_commands.hrl"). --include_lib("stdlib/include/ms_transform.hrl"). -include("mod_privacy.hrl"). +-include("ejabberd_sm.hrl"). + +-callback init() -> ok | {error, any()}. +-callback set_session(#session{}) -> ok. +-callback delete_session(binary(), binary(), binary(), sid()) -> + {ok, #session{}} | {error, notfound}. +-callback get_sessions() -> [#session{}]. +-callback get_sessions(binary()) -> [#session{}]. +-callback get_sessions(binary(), binary()) -> [#session{}]. +-callback get_sessions(binary(), binary(), binary()) -> [#session{}]. --record(session, {sid, usr, us, priority, info}). --record(session_counter, {vhost, count}). -record(state, {}). %% default value for the maximum number of user connections @@ -90,14 +98,13 @@ %% Function: start_link() -> {ok,Pid} | ignore | {error,Error} %% Description: Starts the server %%-------------------------------------------------------------------- --type sid() :: {erlang:timestamp(), pid()}. --type ip() :: {inet:ip_address(), inet:port_number()} | undefined. --type info() :: [{conn, atom()} | {ip, ip()} | {node, atom()} - | {oor, boolean()} | {auth_module, atom()}]. --type prio() :: undefined | integer(). - -export_type([sid/0]). +start() -> + ChildSpec = {?MODULE, {?MODULE, start_link, []}, + transient, 1000, worker, [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). @@ -116,8 +123,6 @@ route(From, To, Packet) -> open_session(SID, User, Server, Resource, Priority, Info) -> set_session(SID, User, Server, Resource, Priority, Info), - mnesia:dirty_update_counter(session_counter, - jlib:nameprep(Server), 1), check_for_sessions_to_replace(User, Server, Resource), JID = jlib:make_jid(User, Server, Resource), ejabberd_hooks:run(sm_register_connection_hook, @@ -131,16 +136,14 @@ open_session(SID, User, Server, Resource, Info) -> -spec close_session(sid(), binary(), binary(), binary()) -> ok. close_session(SID, User, Server, Resource) -> - Info = case mnesia:dirty_read({session, SID}) of - [] -> []; - [#session{info=I}] -> I - end, - F = fun() -> - mnesia:delete({session, SID}), - mnesia:dirty_update_counter(session_counter, - jlib:nameprep(Server), -1) - end, - mnesia:sync_dirty(F), + Mod = get_sm_backend(), + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + LResource = jlib:resourceprep(Resource), + Info = case Mod:delete_session(LUser, LServer, LResource, SID) of + {ok, #session{info = I}} -> I; + {error, notfound} -> [] + end, JID = jlib:make_jid(User, Server, Resource), ejabberd_hooks:run(sm_remove_connection_hook, JID#jid.lserver, [SID, JID, Info]). @@ -169,27 +172,17 @@ disconnect_removed_user(User, Server) -> get_user_resources(User, Server) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - case catch mnesia:dirty_index_read(session, US, #session.us) of - {'EXIT', _Reason} -> - []; - Ss -> - [element(3, S#session.usr) || S <- clean_session_list(Ss)] - end. + Mod = get_sm_backend(), + Ss = Mod:get_sessions(LUser, LServer), + [element(3, S#session.usr) || S <- clean_session_list(Ss)]. -spec get_user_present_resources(binary(), binary()) -> [tuple()]. get_user_present_resources(LUser, LServer) -> - US = {LUser, LServer}, - case catch mnesia:dirty_index_read(session, US, - #session.us) - of - {'EXIT', _Reason} -> []; - Ss -> - [{S#session.priority, element(3, S#session.usr)} - || S <- clean_session_list(Ss), - is_integer(S#session.priority)] - end. + Mod = get_sm_backend(), + Ss = Mod:get_sessions(LUser, LServer), + [{S#session.priority, element(3, S#session.usr)} + || S <- clean_session_list(Ss), is_integer(S#session.priority)]. -spec get_user_ip(binary(), binary(), binary()) -> ip(). @@ -197,8 +190,8 @@ get_user_ip(User, Server, Resource) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), LResource = jlib:resourceprep(Resource), - USR = {LUser, LServer, LResource}, - case mnesia:dirty_index_read(session, USR, #session.usr) of + Mod = get_sm_backend(), + case Mod:get_sessions(LUser, LServer, LResource) of [] -> undefined; Ss -> @@ -212,8 +205,8 @@ get_user_info(User, Server, Resource) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), LResource = jlib:resourceprep(Resource), - USR = {LUser, LServer, LResource}, - case mnesia:dirty_index_read(session, USR, #session.usr) of + Mod = get_sm_backend(), + case Mod:get_sessions(LUser, LServer, LResource) of [] -> offline; Ss -> @@ -262,8 +255,8 @@ get_session_pid(User, Server, Resource) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), LResource = jlib:resourceprep(Resource), - USR = {LUser, LServer, LResource}, - case catch mnesia:dirty_index_read(session, USR, #session.usr) of + Mod = get_sm_backend(), + case Mod:get_sessions(LUser, LServer, LResource) of [#session{sid = {_, Pid}}] -> Pid; _ -> none end. @@ -271,49 +264,30 @@ get_session_pid(User, Server, Resource) -> -spec dirty_get_sessions_list() -> [ljid()]. dirty_get_sessions_list() -> - mnesia:dirty_select( - session, - [{#session{usr = '$1', _ = '_'}, - [], - ['$1']}]). + Mod = get_sm_backend(), + [S#session.usr || S <- Mod:get_sessions()]. dirty_get_my_sessions_list() -> - mnesia:dirty_select( - session, - [{#session{sid = {'_', '$1'}, _ = '_'}, - [{'==', {node, '$1'}, node()}], - ['$_']}]). + Mod = get_sm_backend(), + [S || S <- Mod:get_sessions(), node(element(2, S#session.sid)) == node()]. -spec get_vh_session_list(binary()) -> [ljid()]. get_vh_session_list(Server) -> LServer = jlib:nameprep(Server), - mnesia:dirty_select(session, - [{#session{usr = '$1', _ = '_'}, - [{'==', {element, 2, '$1'}, LServer}], ['$1']}]). + Mod = get_sm_backend(), + [S#session.usr || S <- Mod:get_sessions(LServer)]. -spec get_all_pids() -> [pid()]. get_all_pids() -> - mnesia:dirty_select( - session, - ets:fun2ms( - fun(#session{sid = {_, Pid}}) -> - Pid - end)). + Mod = get_sm_backend(), + [element(2, S#session.sid) || S <- Mod:get_sessions()]. get_vh_session_number(Server) -> LServer = jlib:nameprep(Server), - Query = mnesia:dirty_select( - session_counter, - [{#session_counter{vhost = LServer, count = '$1'}, - [], - ['$1']}]), - case Query of - [Count] -> - Count; - _ -> 0 - end. + Mod = get_sm_backend(), + length(Mod:get_sessions(LServer)). register_iq_handler(Host, XMLNS, Module, Fun) -> ejabberd_sm ! @@ -343,18 +317,8 @@ unregister_iq_handler(Host, XMLNS) -> %% Description: Initiates the server %%-------------------------------------------------------------------- init([]) -> - update_tables(), - mnesia:create_table(session, - [{ram_copies, [node()]}, - {attributes, record_info(fields, session)}]), - mnesia:create_table(session_counter, - [{ram_copies, [node()]}, - {attributes, record_info(fields, session_counter)}]), - mnesia:add_table_index(session, usr), - mnesia:add_table_index(session, us), - mnesia:add_table_copy(session, node(), ram_copies), - mnesia:add_table_copy(session_counter, node(), ram_copies), - mnesia:subscribe(system), + Mod = get_sm_backend(), + Mod:init(), ets:new(sm_iqtable, [named_table]), lists:foreach( fun(Host) -> @@ -366,7 +330,6 @@ init([]) -> ejabberd_sm, disconnect_removed_user, 100) end, ?MYHOSTS), ejabberd_commands:register_commands(commands()), - {ok, #state{}}. %%-------------------------------------------------------------------- @@ -404,9 +367,6 @@ handle_info({route, From, To, Packet}, State) -> ok end, {noreply, State}; -handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> - recount_session_table(Node), - {noreply, State}; handle_info({register_iq_handler, Host, XMLNS, Module, Function}, State) -> ets:insert(sm_iqtable, {{XMLNS, Host}, Module, Function}), {noreply, State}; @@ -454,38 +414,9 @@ set_session(SID, User, Server, Resource, Priority, Info) -> LResource = jlib:resourceprep(Resource), US = {LUser, LServer}, USR = {LUser, LServer, LResource}, - F = fun () -> - mnesia:write(#session{sid = SID, usr = USR, us = US, - priority = Priority, info = Info}) - end, - mnesia:sync_dirty(F). - -%% Recalculates alive sessions when Node goes down -%% and updates session and session_counter tables -recount_session_table(Node) -> - F = fun() -> - Es = mnesia:select( - session, - [{#session{sid = {'_', '$1'}, _ = '_'}, - [{'==', {node, '$1'}, Node}], - ['$_']}]), - lists:foreach(fun(E) -> - mnesia:delete({session, E#session.sid}) - end, Es), - %% reset session_counter table with active sessions - mnesia:clear_table(session_counter), - lists:foreach(fun(Server) -> - LServer = jlib:nameprep(Server), - Hs = mnesia:select(session, - [{#session{usr = '$1', _ = '_'}, - [{'==', {element, 2, '$1'}, LServer}], - ['$1']}]), - mnesia:write( - #session_counter{vhost = LServer, - count = length(Hs)}) - end, ?MYHOSTS) - end, - mnesia:async_dirty(F). + Mod = get_sm_backend(), + Mod:set_session(#session{sid = SID, usr = USR, us = US, + priority = Priority, info = Info}). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -499,8 +430,9 @@ do_route(From, To, {broadcast, _} = Packet) -> end, get_user_resources(To#jid.user, To#jid.server)); _ -> - USR = jlib:jid_tolower(To), - case mnesia:dirty_index_read(session, USR, #session.usr) of + {U, S, R} = jlib:jid_tolower(To), + Mod = get_sm_backend(), + case Mod:get_sessions(U, S, R) of [] -> ?DEBUG("packet dropped~n", []); Ss -> @@ -584,17 +516,35 @@ do_route(From, To, #xmlel{} = Packet) -> PResources); true -> ok end; - <<"message">> -> route_message(From, To, Packet); + <<"message">> -> + case xml:get_attr_s(<<"type">>, Attrs) of + <<"chat">> -> route_message(From, To, Packet, chat); + <<"headline">> -> route_message(From, To, Packet, headline); + <<"error">> -> ok; + <<"groupchat">> -> + Err = jlib:make_error_reply(Packet, + ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(To, From, Err); + _ -> + route_message(From, To, Packet, normal) + end; <<"iq">> -> process_iq(From, To, Packet); _ -> ok end; _ -> - USR = {LUser, LServer, LResource}, - case mnesia:dirty_index_read(session, USR, #session.usr) - of + Mod = get_sm_backend(), + case Mod:get_sessions(LUser, LServer, LResource) of [] -> case Name of - <<"message">> -> route_message(From, To, Packet); + <<"message">> -> + case xml:get_attr_s(<<"type">>, Attrs) of + <<"chat">> -> route_message(From, To, Packet, chat); + <<"error">> -> ok; + _ -> + Err = jlib:make_error_reply(Packet, + ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(To, From, Err) + end; <<"iq">> -> case xml:get_attr_s(<<"type">>, Attrs) of <<"error">> -> ok; @@ -604,7 +554,7 @@ do_route(From, To, #xmlel{} = Packet) -> ?ERR_SERVICE_UNAVAILABLE), ejabberd_router:route(To, From, Err) end; - _ -> ?DEBUG("packet droped~n", []) + _ -> ?DEBUG("packet dropped~n", []) end; Ss -> Session = lists:max(Ss), @@ -637,19 +587,19 @@ is_privacy_allow(From, To, Packet, PrivacyList) -> [User, Server, PrivacyList, {From, To, Packet}, in]). -route_message(From, To, Packet) -> +route_message(From, To, Packet, Type) -> LUser = To#jid.luser, LServer = To#jid.lserver, PrioRes = get_user_present_resources(LUser, LServer), case catch lists:max(PrioRes) of {Priority, _R} when is_integer(Priority), Priority >= 0 -> - lists:foreach(fun ({P, R}) when P == Priority -> + lists:foreach(fun ({P, R}) when P == Priority; + (P >= 0) and (Type == headline) -> LResource = jlib:resourceprep(R), - USR = {LUser, LServer, LResource}, - case mnesia:dirty_index_read(session, USR, - #session.usr) - of + Mod = get_sm_backend(), + case Mod:get_sessions(LUser, LServer, + LResource) of [] -> ok; % Race condition Ss -> @@ -663,12 +613,8 @@ route_message(From, To, Packet) -> end, PrioRes); _ -> - case xml:get_tag_attr_s(<<"type">>, Packet) of - <<"error">> -> ok; - <<"groupchat">> -> - bounce_offline_message(From, To, Packet); - <<"headline">> -> - bounce_offline_message(From, To, Packet); + case Type of + headline -> ok; _ -> case ejabberd_auth:is_user_exists(LUser, LServer) of true -> @@ -730,17 +676,15 @@ is_existing_resource(LUser, LServer, LResource) -> [] /= get_resource_sessions(LUser, LServer, LResource). get_resource_sessions(User, Server, Resource) -> - USR = {jlib:nodeprep(User), jlib:nameprep(Server), - jlib:resourceprep(Resource)}, - mnesia:dirty_select(session, - [{#session{sid = '$1', usr = USR, _ = '_'}, [], - ['$1']}]). + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + LResource = jlib:resourceprep(Resource), + Mod = get_sm_backend(), + [S#session.sid || S <- Mod:get_sessions(LUser, LServer, LResource)]. check_max_sessions(LUser, LServer) -> - SIDs = mnesia:dirty_select(session, - [{#session{sid = '$1', us = {LUser, LServer}, - _ = '_'}, - [], ['$1']}]), + Mod = get_sm_backend(), + SIDs = [S#session.sid || S <- Mod:get_sessions(LUser, LServer)], MaxSessions = get_max_user_sessions(LUser, LServer), if length(SIDs) =< MaxSessions -> ok; true -> {_, Pid} = lists:min(SIDs), Pid ! replaced @@ -790,17 +734,24 @@ process_iq(From, To, Packet) -> -spec force_update_presence({binary(), binary()}) -> any(). -force_update_presence({LUser, _LServer} = US) -> - case catch mnesia:dirty_index_read(session, US, - #session.us) - of - {'EXIT', _Reason} -> ok; - Ss -> - lists:foreach(fun (#session{sid = {_, Pid}}) -> - Pid ! {force_update_presence, LUser} - end, - Ss) - end. +force_update_presence({LUser, LServer}) -> + Mod = get_sm_backend(), + Ss = Mod:get_sessions(LUser, LServer), + lists:foreach(fun (#session{sid = {_, Pid}}) -> + Pid ! {force_update_presence, LUser} + end, + Ss). + +-spec get_sm_backend() -> module(). + +get_sm_backend() -> + DBType = ejabberd_config:get_option(sm_db_type, + fun(mnesia) -> mnesia; + (internal) -> mnesia; + (odbc) -> odbc; + (redis) -> redis + end, mnesia), + list_to_atom("ejabberd_sm_" ++ atom_to_list(DBType)). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% ejabberd commands @@ -852,29 +803,3 @@ kick_user(User, Server) -> PID ! kick end, Resources), length(Resources). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%% Update Mnesia tables - -update_tables() -> - case catch mnesia:table_info(session, attributes) of - [ur, user, node] -> mnesia:delete_table(session); - [ur, user, pid] -> mnesia:delete_table(session); - [usr, us, pid] -> mnesia:delete_table(session); - [usr, us, sid, priority, info] -> mnesia:delete_table(session); - [sid, usr, us, priority] -> - mnesia:delete_table(session); - [sid, usr, us, priority, info] -> ok; - {'EXIT', _} -> ok - end, - case lists:member(presence, mnesia:system_info(tables)) - of - true -> mnesia:delete_table(presence); - false -> ok - end, - case lists:member(local_session, mnesia:system_info(tables)) of - true -> - mnesia:delete_table(local_session); - false -> - ok - end. diff --git a/src/ejabberd_sm_mnesia.erl b/src/ejabberd_sm_mnesia.erl new file mode 100644 index 000000000..7acc1022d --- /dev/null +++ b/src/ejabberd_sm_mnesia.erl @@ -0,0 +1,145 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2015, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 9 Mar 2015 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(ejabberd_sm_mnesia). + +-behaviour(gen_server). +-behaviour(ejabberd_sm). + +%% API +-export([init/0, + set_session/1, + delete_session/4, + get_sessions/0, + get_sessions/1, + get_sessions/2, + get_sessions/3]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("ejabberd.hrl"). +-include("ejabberd_sm.hrl"). +-include("jlib.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +-record(state, {}). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec init() -> ok | {error, any()}. +init() -> + case gen_server:start_link({local, ?MODULE}, ?MODULE, [], []) of + {ok, _Pid} -> + ok; + Err -> + Err + end. + +-spec set_session(#session{}) -> ok. +set_session(Session) -> + mnesia:dirty_write(Session). + +-spec delete_session(binary(), binary(), binary(), sid()) -> + {ok, #session{}} | {error, notfound}. +delete_session(_LUser, _LServer, _LResource, SID) -> + case mnesia:dirty_read(session, SID) of + [Session] -> + mnesia:dirty_delete(session, SID), + {ok, Session}; + [] -> + {error, notfound} + end. + +-spec get_sessions() -> [#session{}]. +get_sessions() -> + ets:tab2list(session). + +-spec get_sessions(binary()) -> [#session{}]. +get_sessions(LServer) -> + mnesia:dirty_select(session, + [{#session{usr = '$1', _ = '_'}, + [{'==', {element, 2, '$1'}, LServer}], ['$_']}]). + +-spec get_sessions(binary(), binary()) -> [#session{}]. +get_sessions(LUser, LServer) -> + mnesia:dirty_index_read(session, {LUser, LServer}, #session.us). + +-spec get_sessions(binary(), binary(), binary()) -> [#session{}]. +get_sessions(LUser, LServer, LResource) -> + mnesia:dirty_index_read(session, {LUser, LServer, LResource}, #session.usr). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([]) -> + update_tables(), + mnesia:create_table(session, + [{ram_copies, [node()]}, + {attributes, record_info(fields, session)}]), + mnesia:create_table(session_counter, + [{ram_copies, [node()]}, + {attributes, record_info(fields, session_counter)}]), + mnesia:add_table_index(session, usr), + mnesia:add_table_index(session, us), + mnesia:add_table_copy(session, node(), ram_copies), + mnesia:add_table_copy(session_counter, node(), ram_copies), + mnesia:subscribe(system), + {ok, #state{}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> + ets:select_delete( + session, + ets:fun2ms( + fun(#session{sid = {_, Pid}}) -> + node(Pid) == Node + end)), + {noreply, State}; +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_tables() -> + case catch mnesia:table_info(session, attributes) of + [ur, user, node] -> mnesia:delete_table(session); + [ur, user, pid] -> mnesia:delete_table(session); + [usr, us, pid] -> mnesia:delete_table(session); + [usr, us, sid, priority, info] -> mnesia:delete_table(session); + [sid, usr, us, priority] -> + mnesia:delete_table(session); + [sid, usr, us, priority, info] -> ok; + {'EXIT', _} -> ok + end, + case lists:member(presence, mnesia:system_info(tables)) + of + true -> mnesia:delete_table(presence); + false -> ok + end, + case lists:member(local_session, mnesia:system_info(tables)) of + true -> + mnesia:delete_table(local_session); + false -> + ok + end. diff --git a/src/ejabberd_sm_odbc.erl b/src/ejabberd_sm_odbc.erl new file mode 100644 index 000000000..946f58ffa --- /dev/null +++ b/src/ejabberd_sm_odbc.erl @@ -0,0 +1,169 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2015, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 9 Mar 2015 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(ejabberd_sm_odbc). + +-behaviour(ejabberd_sm). + +%% API +-export([init/0, + set_session/1, + delete_session/4, + get_sessions/0, + get_sessions/1, + get_sessions/2, + get_sessions/3]). + +-include("ejabberd.hrl"). +-include("ejabberd_sm.hrl"). +-include("logger.hrl"). +-include("jlib.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec init() -> ok | {error, any()}. +init() -> + Node = ejabberd_odbc:escape(jlib:atom_to_binary(node())), + ?INFO_MSG("Cleaning SQL SM table...", []), + lists:foldl( + fun(Host, ok) -> + case ejabberd_odbc:sql_query( + Host, [<<"delete from sm where node='">>, Node, <<"'">>]) of + {updated, _} -> + ok; + Err -> + ?ERROR_MSG("failed to clean 'sm' table: ~p", [Err]), + Err + end; + (_, Err) -> + Err + end, ok, ?MYHOSTS). + +set_session(#session{sid = {Now, Pid}, usr = {U, LServer, R}, + priority = Priority, info = Info}) -> + Username = ejabberd_odbc:escape(U), + Resource = ejabberd_odbc:escape(R), + InfoS = ejabberd_odbc:encode_term(Info), + PrioS = enc_priority(Priority), + TS = now_to_timestamp(Now), + PidS = list_to_binary(erlang:pid_to_list(Pid)), + Node = ejabberd_odbc:escape(jlib:atom_to_binary(node(Pid))), + case odbc_queries:update( + LServer, + <<"sm">>, + [<<"usec">>, <<"pid">>, <<"node">>, <<"username">>, + <<"resource">>, <<"priority">>, <<"info">>], + [TS, PidS, Node, Username, Resource, PrioS, InfoS], + [<<"usec='">>, TS, <<"' and pid='">>, PidS, <<"'">>]) of + ok -> + ok; + Err -> + ?ERROR_MSG("failed to update 'sm' table: ~p", [Err]) + end. + +delete_session(_LUser, LServer, _LResource, {Now, Pid}) -> + TS = now_to_timestamp(Now), + PidS = list_to_binary(erlang:pid_to_list(Pid)), + case ejabberd_odbc:sql_query( + LServer, + [<<"select usec, pid, username, resource, priority, info ">>, + <<"from sm where usec='">>, TS, <<"' and pid='">>,PidS, <<"'">>]) of + {selected, _, [Row]} -> + ejabberd_odbc:sql_query( + LServer, [<<"delete from sm where usec='">>, + TS, <<"' and pid='">>, PidS, <<"'">>]), + {ok, row_to_session(LServer, Row)}; + {selected, _, []} -> + {error, notfound}; + Err -> + ?ERROR_MSG("failed to delete from 'sm' table: ~p", [Err]), + {error, notfound} + end. + +get_sessions() -> + lists:flatmap( + fun(LServer) -> + get_sessions(LServer) + end, ?MYHOSTS). + +get_sessions(LServer) -> + case ejabberd_odbc:sql_query( + LServer, [<<"select usec, pid, username, ">>, + <<"resource, priority, info from sm">>]) of + {selected, _, Rows} -> + [row_to_session(LServer, Row) || Row <- Rows]; + Err -> + ?ERROR_MSG("failed to select from 'sm' table: ~p", [Err]), + [] + end. + +get_sessions(LUser, LServer) -> + Username = ejabberd_odbc:escape(LUser), + case ejabberd_odbc:sql_query( + LServer, [<<"select usec, pid, username, ">>, + <<"resource, priority, info from sm where ">>, + <<"username='">>, Username, <<"'">>]) of + {selected, _, Rows} -> + [row_to_session(LServer, Row) || Row <- Rows]; + Err -> + ?ERROR_MSG("failed to select from 'sm' table: ~p", [Err]), + [] + end. + +get_sessions(LUser, LServer, LResource) -> + Username = ejabberd_odbc:escape(LUser), + Resource = ejabberd_odbc:escape(LResource), + case ejabberd_odbc:sql_query( + LServer, [<<"select usec, pid, username, ">>, + <<"resource, priority, info from sm where ">>, + <<"username='">>, Username, <<"' and resource='">>, + Resource, <<"'">>]) of + {selected, _, Rows} -> + [row_to_session(LServer, Row) || Row <- Rows]; + Err -> + ?ERROR_MSG("failed to select from 'sm' table: ~p", [Err]), + [] + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +now_to_timestamp({MSec, Sec, USec}) -> + jlib:integer_to_binary((MSec * 1000000 + Sec) * 1000000 + USec). + +timestamp_to_now(TS) -> + I = jlib:binary_to_integer(TS), + Head = I div 1000000, + USec = I rem 1000000, + MSec = Head div 1000000, + Sec = Head div 1000000, + {MSec, Sec, USec}. + +dec_priority(Prio) -> + case catch jlib:binary_to_integer(Prio) of + {'EXIT', _} -> + undefined; + Int -> + Int + end. + +enc_priority(undefined) -> + <<"">>; +enc_priority(Int) when is_integer(Int) -> + jlib:integer_to_binary(Int). + +row_to_session(LServer, [USec, PidS, User, Resource, PrioS, InfoS]) -> + Now = timestamp_to_now(USec), + Pid = erlang:list_to_pid(binary_to_list(PidS)), + Priority = dec_priority(PrioS), + Info = ejabberd_odbc:decode_term(InfoS), + #session{sid = {Now, Pid}, us = {User, LServer}, + usr = {User, LServer, Resource}, + priority = Priority, + info = Info}. diff --git a/src/ejabberd_sm_redis.erl b/src/ejabberd_sm_redis.erl new file mode 100644 index 000000000..0283f9c3e --- /dev/null +++ b/src/ejabberd_sm_redis.erl @@ -0,0 +1,209 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2015, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 11 Mar 2015 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(ejabberd_sm_redis). + +-behaviour(ejabberd_sm). + +%% API +-export([init/0, + set_session/1, + delete_session/4, + get_sessions/0, + get_sessions/1, + get_sessions/2, + get_sessions/3]). + +-include("ejabberd.hrl"). +-include("ejabberd_sm.hrl"). +-include("logger.hrl"). +-include("jlib.hrl"). + +-define(PROCNAME, 'ejabberd_redis_client'). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec init() -> ok | {error, any()}. +init() -> + Server = ejabberd_config:get_option(redis_server, + fun iolist_to_list/1, + "localhost"), + Port = ejabberd_config:get_option(redis_port, + fun(P) when is_integer(P), + P>0, P<65536 -> + P + end, 6379), + DB = ejabberd_config:get_option(redis_db, + fun(I) when is_integer(I), I >= 0 -> + I + end, 0), + Pass = ejabberd_config:get_option(redis_password, + fun iolist_to_list/1, + ""), + ReconnTimeout = timer:seconds( + ejabberd_config:get_option( + redis_reconnect_timeout, + fun(I) when is_integer(I), I>0 -> I end, + 1)), + ConnTimeout = timer:seconds( + ejabberd_config:get_option( + redis_connect_timeout, + fun(I) when is_integer(I), I>0 -> I end, + 1)), + case eredis:start_link(Server, Port, DB, Pass, + ReconnTimeout, ConnTimeout) of + {ok, Client} -> + register(?PROCNAME, Client), + clean_table(), + ok; + {error, _} = Err -> + ?ERROR_MSG("failed to start redis client: ~p", [Err]), + Err + end. + +-spec set_session(#session{}) -> ok. +set_session(Session) -> + T = term_to_binary(Session), + USKey = us_to_key(Session#session.us), + SIDKey = sid_to_key(Session#session.sid), + ServKey = server_to_key(element(2, Session#session.us)), + USSIDKey = us_sid_to_key(Session#session.us, Session#session.sid), + case eredis:qp(?PROCNAME, [["HSET", USKey, SIDKey, T], + ["HSET", ServKey, USSIDKey, T]]) of + [{ok, _}, {ok, _}] -> + ok; + Err -> + ?ERROR_MSG("failed to set session for redis: ~p", [Err]) + end. + +-spec delete_session(binary(), binary(), binary(), sid()) -> + {ok, #session{}} | {error, notfound}. +delete_session(LUser, LServer, _LResource, SID) -> + USKey = us_to_key({LUser, LServer}), + case eredis:q(?PROCNAME, ["HGETALL", USKey]) of + {ok, Vals} -> + Ss = decode_session_list(Vals), + case lists:keyfind(SID, #session.sid, Ss) of + false -> + {error, notfound}; + Session -> + SIDKey = sid_to_key(SID), + ServKey = server_to_key(element(2, Session#session.us)), + USSIDKey = us_sid_to_key(Session#session.us, SID), + eredis:qp(?PROCNAME, [["HDEL", USKey, SIDKey], + ["HDEL", ServKey, USSIDKey]]), + {ok, Session} + end; + Err -> + ?ERROR_MSG("failed to delete session from redis: ~p", [Err]), + {error, notfound} + end. + +-spec get_sessions() -> [#session{}]. +get_sessions() -> + lists:flatmap( + fun(LServer) -> + get_sessions(LServer) + end, ?MYHOSTS). + +-spec get_sessions(binary()) -> [#session{}]. +get_sessions(LServer) -> + ServKey = server_to_key(LServer), + case eredis:q(?PROCNAME, ["HGETALL", ServKey]) of + {ok, Vals} -> + decode_session_list(Vals); + Err -> + ?ERROR_MSG("failed to get sessions from redis: ~p", [Err]), + [] + end. + +-spec get_sessions(binary(), binary()) -> [#session{}]. +get_sessions(LUser, LServer) -> + USKey = us_to_key({LUser, LServer}), + case eredis:q(?PROCNAME, ["HGETALL", USKey]) of + {ok, Vals} when is_list(Vals) -> + decode_session_list(Vals); + Err -> + ?ERROR_MSG("failed to get sessions from redis: ~p", [Err]), + [] + end. + +-spec get_sessions(binary(), binary(), binary()) -> [#session{}]. +get_sessions(LUser, LServer, LResource) -> + USKey = us_to_key({LUser, LServer}), + case eredis:q(?PROCNAME, ["HGETALL", USKey]) of + {ok, Vals} when is_list(Vals) -> + [S || S <- decode_session_list(Vals), + element(3, S#session.usr) == LResource]; + Err -> + ?ERROR_MSG("failed to get sessions from redis: ~p", [Err]), + [] + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +iolist_to_list(IOList) -> + binary_to_list(iolist_to_binary(IOList)). + +us_to_key({LUser, LServer}) -> + <<"ejabberd:sm:", LUser/binary, "@", LServer/binary>>. + +server_to_key(LServer) -> + <<"ejabberd:sm:", LServer/binary>>. + +us_sid_to_key(US, SID) -> + term_to_binary({US, SID}). + +sid_to_key(SID) -> + term_to_binary(SID). + +decode_session_list([_, Val|T]) -> + [binary_to_term(Val)|decode_session_list(T)]; +decode_session_list([]) -> + []. + +clean_table() -> + ?INFO_MSG("Cleaning Redis SM table...", []), + lists:foreach( + fun(LServer) -> + ServKey = server_to_key(LServer), + case eredis:q(?PROCNAME, ["HKEYS", ServKey]) of + {ok, []} -> + ok; + {ok, Vals} -> + Vals1 = lists:filter( + fun(USSIDKey) -> + {_, SID} = binary_to_term(USSIDKey), + node(element(2, SID)) == node() + end, Vals), + Q1 = ["HDEL", ServKey | Vals1], + Q2 = lists:map( + fun(USSIDKey) -> + {US, SID} = binary_to_term(USSIDKey), + USKey = us_to_key(US), + SIDKey = sid_to_key(SID), + ["HDEL", USKey, SIDKey] + end, Vals1), + Res = eredis:qp(?PROCNAME, [Q1|Q2]), + case lists:filter( + fun({ok, _}) -> false; + (_) -> true + end, Res) of + [] -> + ok; + Errs -> + ?ERROR_MSG("failed to clean redis table for " + "server ~s: ~p", [LServer, Errs]) + end; + Err -> + ?ERROR_MSG("failed to clean redis table for " + "server ~s: ~p", [LServer, Err]) + end + end, ?MYHOSTS). diff --git a/src/ejabberd_socket.erl b/src/ejabberd_socket.erl index c74ef9aec..29c7774e4 100644 --- a/src/ejabberd_socket.erl +++ b/src/ejabberd_socket.erl @@ -50,15 +50,14 @@ -include("logger.hrl"). -include("jlib.hrl"). --type sockmod() :: ejabberd_http_poll | - ejabberd_http_bind | +-type sockmod() :: ejabberd_http_bind | + ejabberd_http_ws | gen_tcp | p1_tls | ezlib. -type receiver() :: pid () | atom(). -type socket() :: pid() | inet:socket() | p1_tls:tls_socket() | ezlib:zlib_socket() | - ejabberd_http_bind:bind_socket() | - ejabberd_http_poll:poll_socket(). + ejabberd_http_bind:bind_socket(). -record(socket_state, {sockmod = gen_tcp :: sockmod(), socket = self() :: socket(), @@ -191,7 +190,7 @@ send(SocketData, Data) -> %% Can only be called when in c2s StateData#state.xml_socket is true %% This function is used for HTTP bind -%% sockmod=ejabberd_http_poll|ejabberd_http_bind or any custom module +%% sockmod=ejabberd_http_ws|ejabberd_http_bind or any custom module -spec send_xml(socket_state(), xmlel()) -> any(). send_xml(SocketData, Data) -> diff --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl index 35c79f429..da25af2c7 100644 --- a/src/ejabberd_sup.erl +++ b/src/ejabberd_sup.erl @@ -62,13 +62,13 @@ init([]) -> brutal_kill, worker, [ejabberd_router]}, - SM = - {ejabberd_sm, - {ejabberd_sm, start_link, []}, + Router_multicast = + {ejabberd_router_multicast, + {ejabberd_router_multicast, start_link, []}, permanent, brutal_kill, worker, - [ejabberd_sm]}, + [ejabberd_router_multicast]}, S2S = {ejabberd_s2s, {ejabberd_s2s, start_link, []}, @@ -144,14 +144,6 @@ init([]) -> infinity, supervisor, [ejabberd_tmp_sup]}, - HTTPPollSupervisor = - {ejabberd_http_poll_sup, - {ejabberd_tmp_sup, start_link, - [ejabberd_http_poll_sup, ejabberd_http_poll]}, - permanent, - infinity, - supervisor, - [ejabberd_tmp_sup]}, FrontendSocketSupervisor = {ejabberd_frontend_socket_sup, {ejabberd_tmp_sup, start_link, @@ -173,7 +165,7 @@ init([]) -> NodeGroups, SystemMonitor, Router, - SM, + Router_multicast, S2S, Local, Captcha, @@ -183,9 +175,6 @@ init([]) -> S2SOutSupervisor, ServiceSupervisor, HTTPSupervisor, - HTTPPollSupervisor, IQSupervisor, FrontendSocketSupervisor, Listener]}}. - - diff --git a/src/ejabberd_update.erl b/src/ejabberd_update.erl index 5b6795c4b..afcb62225 100644 --- a/src/ejabberd_update.erl +++ b/src/ejabberd_update.erl @@ -138,13 +138,6 @@ build_script(Dir, UpdatedBeams) -> LowLevelScript, [{ejabberd, "", filename:join(Dir, "..")}]), Check1 = case Check of - ok -> - %% This clause is for OTP R14B03 and older. - %% Newer Dialyzer reports a never match pattern; don't worry. - ?DEBUG("script: ~p~n", [Script]), - ?DEBUG("low level script: ~p~n", [LowLevelScript]), - ?DEBUG("check: ~p~n", [Check]), - ok; {ok, []} -> ?DEBUG("script: ~p~n", [Script]), ?DEBUG("low level script: ~p~n", [LowLevelScript]), diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index b6e7a9024..29ecb7346 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -717,10 +717,9 @@ process_admin(Host, auth = {_, _Auth, AJID}, q = Query, lang = Lang}) -> SetAccess = fun (Rs) -> mnesia:transaction(fun () -> - Os = mnesia:select(local_config, - [{{local_config, - {access, - '$1', + Os = mnesia:select(access, + [{{access, + {'$1', Host}, '$2'}, [], @@ -732,9 +731,8 @@ process_admin(Host, lists:foreach(fun ({access, Name, Rules}) -> - mnesia:write({local_config, - {access, - Name, + mnesia:write({access, + {Name, Host}, Rules}) end, @@ -757,8 +755,8 @@ process_admin(Host, end; _ -> nothing end, - Access = ets:select(local_config, - [{{local_config, {access, '$1', Host}, '$2'}, [], + Access = ets:select(access, + [{{access, {'$1', Host}, '$2'}, [], [{{access, '$1', '$2'}}]}]), {NumLines, AccessP} = term_to_paragraph(lists:keysort(2,Access), 80), make_xhtml((?H1GL((?T(<<"Access Rules">>)), @@ -791,8 +789,8 @@ process_admin(Host, end; _ -> nothing end, - AccessRules = ets:select(local_config, - [{{local_config, {access, '$1', Host}, '$2'}, [], + AccessRules = ets:select(access, + [{{access, {'$1', Host}, '$2'}, [], [{{access, '$1', '$2'}}]}]), make_xhtml((?H1GL((?T(<<"Access Rules">>)), <<"AccessRights">>, <<"Access Rights">>)) @@ -1174,8 +1172,8 @@ access_rules_to_xhtml(AccessRules, Lang) -> <<"Add New">>)])])]))]). access_parse_query(Host, Query) -> - AccessRules = ets:select(local_config, - [{{local_config, {access, '$1', Host}, '$2'}, [], + AccessRules = ets:select(access, + [{{access, {'$1', Host}, '$2'}, [], [{{access, '$1', '$2'}}]}]), case lists:keysearch(<<"addnew">>, 1, Query) of {value, _} -> @@ -1203,9 +1201,8 @@ access_parse_delete(AccessRules, Host, Query) -> case lists:member({<<"selected">>, ID}, Query) of true -> mnesia:transaction(fun () -> - mnesia:delete({local_config, - {access, - Name, + mnesia:delete({access, + {Name, Host}}) end); _ -> ok @@ -1552,9 +1549,7 @@ user_info(User, Server, Query, Lang) -> c2s_compressed_tls -> <<"tls+zlib">>; http_bind -> - <<"http-bind">>; - http_poll -> - <<"http-poll">> + <<"http-bind">> end, <<" (", ConnS/binary, "://", @@ -2882,4 +2877,3 @@ make_menu_item(item, 3, URI, Name, Lang) -> %%%================================== %%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: - diff --git a/src/ejabberd_websocket.erl b/src/ejabberd_websocket.erl new file mode 100644 index 000000000..9d5f32c33 --- /dev/null +++ b/src/ejabberd_websocket.erl @@ -0,0 +1,404 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_websocket.erl +%%% Author : Eric Cestari <ecestari@process-one.net> +%%% Purpose : XMPP Websocket support +%%% Created : 09-10-2010 by Eric Cestari <ecestari@process-one.net> +%%% +%%% Some code lifted from MISULTIN - WebSocket misultin_websocket.erl - >-|-|-(°> +%%% (http://github.com/ostinelli/misultin/blob/master/src/misultin_websocket.erl) +%%% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>, Joe Armstrong. +%%% All rights reserved. +%%% +%%% Code portions from Joe Armstrong have been originally taken under MIT license at the address: +%%% <http://armstrongonsoftware.blogspot.com/2009/12/comet-is-dead-long-live-websockets.html> +%%% +%%% BSD License +%%% +%%% Redistribution and use in source and binary forms, with or without modification, are permitted provided +%%% that the following conditions are met: +%%% +%%% * Redistributions of source code must retain the above copyright notice, this list of conditions and the +%%% following disclaimer. +%%% * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and +%%% the following disclaimer in the documentation and/or other materials provided with the distribution. +%%% * Neither the name of the authors nor the names of its contributors may be used to endorse or promote +%%% products derived from this software without specific prior written permission. +%%% +%%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED +%%% WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +%%% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +%%% ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +%%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +%%% HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +%%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +%%% POSSIBILITY OF SUCH DAMAGE. +%%% ========================================================================================================== +%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%%---------------------------------------------------------------------- + +-module(ejabberd_websocket). + +-author('ecestari@process-one.net'). + +-export([check/2, socket_handoff/8]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). + +-include("jlib.hrl"). + +-include("ejabberd_http.hrl"). + +-define(CT_XML, {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). +-define(CT_PLAIN, {<<"Content-Type">>, <<"text/plain">>}). + +-define(AC_ALLOW_ORIGIN, {<<"Access-Control-Allow-Origin">>, <<"*">>}). +-define(AC_ALLOW_METHODS, {<<"Access-Control-Allow-Methods">>, <<"GET, OPTIONS">>}). +-define(AC_ALLOW_HEADERS, {<<"Access-Control-Allow-Headers">>, <<"Content-Type">>}). +-define(AC_MAX_AGE, {<<"Access-Control-Max-Age">>, <<"86400">>}). + +-define(OPTIONS_HEADER, [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, + ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). +-define(HEADER, [?CT_XML, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). + +check(_Path, Headers) -> + RequiredHeaders = [{'Upgrade', <<"websocket">>}, + {'Connection', ignore}, {'Host', ignore}, + {<<"Sec-Websocket-Key">>, ignore}, + {<<"Sec-Websocket-Version">>, <<"13">>}], + + F = fun ({Tag, Val}) -> + case lists:keyfind(Tag, 1, Headers) of + false -> true; % header not found, keep in list + {_, HVal} -> + case Val of + ignore -> false; % ignore value -> ok, remove from list + _ -> + % expected value -> ok, remove from list (false) + % value is different, keep in list (true) + str:to_lower(HVal) /= Val + end + end + end, + case lists:filter(F, RequiredHeaders) of + [] -> true; + _MissingHeaders -> false + end. + +socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path, + headers = Headers, host = Host, port = Port}, + Socket, SockMod, Buf, _Opts, HandlerModule, InfoMsgFun) -> + case check(LocalPath, Headers) of + true -> + WS = #ws{socket = Socket, + sockmod = SockMod, + ip = IP, + q = Q, + host = Host, + port = Port, + path = Path, + headers = Headers, + local_path = LocalPath, + buf = Buf}, + + connect(WS, HandlerModule); + _ -> + {200, ?HEADER, InfoMsgFun()} + end; +socket_handoff(_, #request{method = 'OPTIONS'}, _, _, _, _, _, _) -> + {200, ?OPTIONS_HEADER, []}; +socket_handoff(_, #request{method = 'HEAD'}, _, _, _, _, _, _) -> + {200, ?HEADER, []}; +socket_handoff(_, _, _, _, _, _, _, _) -> + {400, ?HEADER, #xmlel{name = <<"h1">>, + children = [{xmlcdata, <<"400 Bad Request">>}]}}. + +connect(#ws{socket = Socket, sockmod = SockMod} = Ws, WsLoop) -> + {NewWs, HandshakeResponse} = handshake(Ws), + SockMod:send(Socket, HandshakeResponse), + + ?DEBUG("Sent handshake response : ~p", + [HandshakeResponse]), + Ws0 = {Ws, self()}, + {ok, WsHandleLoopPid} = WsLoop:start_link(Ws0), + erlang:monitor(process, WsHandleLoopPid), + + case NewWs#ws.buf of + <<>> -> + ok; + Data -> + self() ! {raw, Socket, Data} + end, + + % set opts + case SockMod of + gen_tcp -> + inet:setopts(Socket, [{packet, 0}, {active, true}]); + _ -> + SockMod:setopts(Socket, [{packet, 0}, {active, true}]) + end, + ws_loop(none, Socket, WsHandleLoopPid, SockMod). + +handshake(#ws{headers = Headers} = State) -> + {_, Key} = lists:keyfind(<<"Sec-Websocket-Key">>, 1, + Headers), + SubProtocolHeader = case find_subprotocol(Headers) of + false -> + []; + V -> + [<<"Sec-Websocket-Protocol:">>, V, <<"\r\n">>] + end, + Hash = jlib:encode_base64( + p1_sha:sha1(<<Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11">>)), + {State, [<<"HTTP/1.1 101 Switching Protocols\r\n">>, + <<"Upgrade: websocket\r\n">>, + <<"Connection: Upgrade\r\n">>, + SubProtocolHeader, + <<"Sec-WebSocket-Accept: ">>, Hash, <<"\r\n\r\n">>]}. + +find_subprotocol(Headers) -> + case lists:keysearch(<<"Sec-Websocket-Protocol">>, 1, Headers) of + false -> + case lists:keysearch(<<"Websocket-Protocol">>, 1, Headers) of + false -> + false; + {value, {_, Protocol2}} -> + Protocol2 + end; + {value, {_, Protocol}} -> + Protocol + end. + + +ws_loop(FrameInfo, Socket, WsHandleLoopPid, SocketMode) -> + receive + {DataType, _Socket, Data} when DataType =:= tcp orelse DataType =:= raw -> + case handle_data(DataType, FrameInfo, Data, Socket, WsHandleLoopPid, SocketMode) of + {error, Error} -> + ?DEBUG("tls decode error ~p", [Error]), + websocket_close(Socket, WsHandleLoopPid, SocketMode, 1002); % protocol error + {NewFrameInfo, ToSend} -> + lists:foreach(fun(Pkt) -> SocketMode:send(Socket, Pkt) + end, ToSend), + ws_loop(NewFrameInfo, Socket, WsHandleLoopPid, SocketMode) + end; + {tcp_closed, _Socket} -> + ?DEBUG("tcp connection was closed, exit", []), + websocket_close(Socket, WsHandleLoopPid, SocketMode, 0); + {'DOWN', Ref, process, WsHandleLoopPid, Reason} -> + Code = case Reason of + normal -> + 1000; % normal close + _ -> + ?ERROR_MSG("linked websocket controlling loop crashed " + "with reason: ~p", + [Reason]), + 1011 % internal error + end, + erlang:demonitor(Ref), + websocket_close(Socket, WsHandleLoopPid, SocketMode, Code); + {send, Data} -> + SocketMode:send(Socket, encode_frame(Data, 1)), + ws_loop(FrameInfo, Socket, WsHandleLoopPid, + SocketMode); + {ping, Data} -> + SocketMode:send(Socket, encode_frame(Data, 9)), + ws_loop(FrameInfo, Socket, WsHandleLoopPid, + SocketMode); + shutdown -> + ?DEBUG("shutdown request received, closing websocket " + "with pid ~p", + [self()]), + websocket_close(Socket, WsHandleLoopPid, SocketMode, 1001); % going away + _Ignored -> + ?WARNING_MSG("received unexpected message, ignoring: ~p", + [_Ignored]), + ws_loop(FrameInfo, Socket, WsHandleLoopPid, + SocketMode) + end. + +encode_frame(Data, Opcode) -> + case byte_size(Data) of + S1 when S1 < 126 -> + <<1:1, 0:3, Opcode:4, 0:1, S1:7, Data/binary>>; + S2 when S2 < 65536 -> + <<1:1, 0:3, Opcode:4, 0:1, 126:7, S2:16, Data/binary>>; + S3 -> + <<1:1, 0:3, Opcode:4, 0:1, 127:7, S3:64, Data/binary>> + end. + +-record(frame_info, + {mask = none, offset = 0, left, final_frame = true, + opcode, unprocessed = <<>>, unmasked = <<>>, + unmasked_msg = <<>>}). + +decode_header(<<Final:1, _:3, Opcode:4, 0:1, + Len:7, Data/binary>>) + when Len < 126 -> + {Len, Final, Opcode, none, Data}; +decode_header(<<Final:1, _:3, Opcode:4, 0:1, + 126:7, Len:16/integer, Data/binary>>) -> + {Len, Final, Opcode, none, Data}; +decode_header(<<Final:1, _:3, Opcode:4, 0:1, + 127:7, Len:64/integer, Data/binary>>) -> + {Len, Final, Opcode, none, Data}; +decode_header(<<Final:1, _:3, Opcode:4, 1:1, + Len:7, Mask:4/binary, Data/binary>>) + when Len < 126 -> + {Len, Final, Opcode, Mask, Data}; +decode_header(<<Final:1, _:3, Opcode:4, 1:1, + 126:7, Len:16/integer, Mask:4/binary, Data/binary>>) -> + {Len, Final, Opcode, Mask, Data}; +decode_header(<<Final:1, _:3, Opcode:4, 1:1, + 127:7, Len:64/integer, Mask:4/binary, Data/binary>>) -> + {Len, Final, Opcode, Mask, Data}; +decode_header(_) -> none. + +unmask_int(Offset, _, <<>>, Acc) -> + {Acc, Offset}; +unmask_int(0, <<M:32>> = Mask, + <<N:32, Rest/binary>>, Acc) -> + unmask_int(0, Mask, Rest, + <<Acc/binary, (M bxor N):32>>); +unmask_int(0, <<M:8, _/binary>> = Mask, + <<N:8, Rest/binary>>, Acc) -> + unmask_int(1, Mask, Rest, + <<Acc/binary, (M bxor N):8>>); +unmask_int(1, <<_:8, M:8, _/binary>> = Mask, + <<N:8, Rest/binary>>, Acc) -> + unmask_int(2, Mask, Rest, + <<Acc/binary, (M bxor N):8>>); +unmask_int(2, <<_:16, M:8, _/binary>> = Mask, + <<N:8, Rest/binary>>, Acc) -> + unmask_int(3, Mask, Rest, + <<Acc/binary, (M bxor N):8>>); +unmask_int(3, <<_:24, M:8>> = Mask, + <<N:8, Rest/binary>>, Acc) -> + unmask_int(0, Mask, Rest, + <<Acc/binary, (M bxor N):8>>). + +unmask(#frame_info{mask = none} = State, Data) -> + {State, Data}; +unmask(#frame_info{mask = Mask, offset = Offset} = State, Data) -> + {Unmasked, NewOffset} = unmask_int(Offset, Mask, + Data, <<>>), + {State#frame_info{offset = NewOffset}, Unmasked}. + +process_frame(none, Data) -> + process_frame(#frame_info{}, Data); +process_frame(#frame_info{left = Left} = FrameInfo, <<>>) when Left > 0 -> + {FrameInfo, [], []}; +process_frame(#frame_info{unprocessed = none, + unmasked = UnmaskedPre, left = Left} = + State, + Data) + when byte_size(Data) < Left -> + {State2, Unmasked} = unmask(State, Data), + {State2#frame_info{left = Left - byte_size(Data), + unmasked = [UnmaskedPre, Unmasked]}, + [], []}; +process_frame(#frame_info{unprocessed = none, + unmasked = UnmaskedPre, opcode = Opcode, + final_frame = Final, left = Left, + unmasked_msg = UnmaskedMsg} = + FrameInfo, + Data) -> + <<ToProcess:(Left)/binary, Unprocessed/binary>> = Data, + {_, Unmasked} = unmask(FrameInfo, ToProcess), + case Final of + true -> + {FrameInfo3, Recv, Send} = process_frame(#frame_info{}, + Unprocessed), + case Opcode of + X when X < 3 -> + {FrameInfo3, + [iolist_to_binary([UnmaskedMsg, UnmaskedPre, Unmasked]) + | Recv], + Send}; + 9 -> % Ping + Frame = encode_frame(Unprocessed, 10), + {FrameInfo3#frame_info{unmasked_msg = UnmaskedMsg}, [ping | Recv], + [Frame | Send]}; + 10 -> % Pong + {FrameInfo3, [pong | Recv], Send}; + 8 -> % Close + CloseCode = case Unmasked of + <<Code:16/integer-big, Message/binary>> -> + ?DEBUG("WebSocket close op: ~p ~s", + [Code, Message]), + Code; + <<Code:16/integer-big>> -> + ?DEBUG("WebSocket close op: ~p", [Code]), + Code; + _ -> + ?DEBUG("WebSocket close op unknown: ~p", + [Unmasked]), + 1000 + end, + + Frame = encode_frame(<<CloseCode:16/integer-big>>, 8), + {FrameInfo3#frame_info{unmasked_msg=UnmaskedMsg}, Recv, + [Frame | Send]}; + _ -> + {FrameInfo3#frame_info{unmasked_msg = UnmaskedMsg}, Recv, + Send} + end; + _ -> + process_frame(#frame_info{unmasked_msg = + [UnmaskedMsg, UnmaskedPre, + Unmasked]}, + Unprocessed) + end; +process_frame(#frame_info{unprocessed = <<>>} = + FrameInfo, + Data) -> + case decode_header(Data) of + none -> + {FrameInfo#frame_info{unprocessed = Data}, [], []}; + {Len, Final, Opcode, Mask, Rest} -> + process_frame(FrameInfo#frame_info{mask = Mask, + final_frame = Final == 1, + left = Len, opcode = Opcode, + unprocessed = none}, + Rest) + end; +process_frame(#frame_info{unprocessed = + UnprocessedPre} = + FrameInfo, + Data) -> + process_frame(FrameInfo#frame_info{unprocessed = <<>>}, + <<UnprocessedPre/binary, Data/binary>>). + +handle_data(tcp, FrameInfo, Data, Socket, WsHandleLoopPid, p1_tls) -> + case p1_tls:recv_data(Socket, Data) of + {ok, NewData} -> + handle_data_int(FrameInfo, NewData, Socket, WsHandleLoopPid, p1_tls); + {error, Error} -> + {error, Error} + end; +handle_data(_, FrameInfo, Data, Socket, WsHandleLoopPid, SockMod) -> + handle_data_int(FrameInfo, Data, Socket, WsHandleLoopPid, SockMod). + +handle_data_int(FrameInfo, Data, _Socket, WsHandleLoopPid, _SocketMode) -> + {NewFrameInfo, Recv, Send} = process_frame(FrameInfo, Data), + lists:foreach(fun (El) -> + case El of + pong -> + WsHandleLoopPid ! pong; + ping -> + WsHandleLoopPid ! ping; + _ -> + WsHandleLoopPid ! {received, El} + end + end, + Recv), + {NewFrameInfo, Send}. + +websocket_close(Socket, WsHandleLoopPid, + SocketMode, CloseCode) when CloseCode > 0 -> + Frame = encode_frame(<<CloseCode:16/integer-big>>, 8), + SocketMode:send(Socket, Frame), + websocket_close(Socket, WsHandleLoopPid, SocketMode, 0); +websocket_close(Socket, WsHandleLoopPid, SocketMode, _CloseCode) -> + WsHandleLoopPid ! closed, + SocketMode:close(Socket). diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl index b1bd164a6..904604fc9 100644 --- a/src/ejabberd_xmlrpc.erl +++ b/src/ejabberd_xmlrpc.erl @@ -444,6 +444,8 @@ 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_list(Arg) -> Arg; format_arg(Arg, string) when is_binary(Arg) -> binary_to_list(Arg); +format_arg(undefined, binary) -> <<>>; +format_arg(undefined, string) -> ""; 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}). diff --git a/src/eldap.erl b/src/eldap.erl index c07ddc07b..5e084b01b 100644 --- a/src/eldap.erl +++ b/src/eldap.erl @@ -139,8 +139,8 @@ passwd = <<"">> :: binary(), id = 0 :: non_neg_integer(), bind_timer = make_ref() :: reference(), - dict = dict:new() :: dict(), - req_q = queue:new() :: queue()}). + dict = dict:new() :: ?TDICT, + req_q = queue:new() :: ?TQUEUE}). %%%---------------------------------------------------------------------- %%% API diff --git a/src/ext_mod.erl b/src/ext_mod.erl new file mode 100644 index 000000000..b2b426cec --- /dev/null +++ b/src/ext_mod.erl @@ -0,0 +1,486 @@ +%%%---------------------------------------------------------------------- +%%% File : ext_mod.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : external modules management +%%% Created : 19 Feb 2015 by Christophe Romain <christophe.romain@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2006-2015 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(ext_mod). +-author("Christophe Romain <christophe.romain@process-one.net>"). + +%% Packaging service +-export([start/0, stop/0, update/0, check/1, + available_command/0, available/0, available/1, + installed_command/0, installed/0, installed/1, + install/1, uninstall/1, + upgrade/0, upgrade/1, + add_sources/2, del_sources/1]). + +-include("ejabberd_commands.hrl"). + +-define(REPOS, "https://github.com/processone/ejabberd-contrib"). + +%% -- ejabberd init and commands + +start() -> + case is_contrib_allowed() of + true -> + [code:add_patha(module_ebin_dir(Module)) + || {Module, _} <- installed()], + application:start(inets), + ejabberd_commands:register_commands(commands()); + false -> + ok + end. + +stop() -> + ejabberd_commands:unregister_commands(commands()). + +commands() -> + [#ejabberd_commands{name = modules_update_specs, + tags = [admin,modules], + desc = "", + longdesc = "", + module = ?MODULE, function = update, + args = [], + result = {res, integer}}, + #ejabberd_commands{name = modules_available, + tags = [admin,modules], + desc = "", + longdesc = "", + module = ?MODULE, function = available_command, + args = [], + result = {modules, {list, + {module, {tuple, + [{name, atom}, + {summary, string}]}}}}}, + #ejabberd_commands{name = modules_installed, + tags = [admin,modules], + desc = "", + longdesc = "", + module = ?MODULE, function = installed_command, + args = [], + result = {modules, {list, + {module, {tuple, + [{name, atom}, + {summary, string}]}}}}}, + #ejabberd_commands{name = module_install, + tags = [admin,modules], + desc = "", + longdesc = "", + module = ?MODULE, function = install, + args = [{module, binary}], + result = {res, integer}}, + #ejabberd_commands{name = module_uninstall, + tags = [admin,modules], + desc = "", + longdesc = "", + module = ?MODULE, function = uninstall, + args = [{module, binary}], + result = {res, integer}}, + #ejabberd_commands{name = module_upgrade, + tags = [admin,modules], + desc = "", + longdesc = "", + module = ?MODULE, function = upgrade, + args = [{module, binary}], + result = {res, integer}}, + #ejabberd_commands{name = module_check, + tags = [admin,modules], + desc = "", + longdesc = "", + module = ?MODULE, function = check, + args = [{module, binary}], + result = {res, integer}} + ]. +%% -- public modules functions + +update() -> + add_sources(?REPOS), + lists:foreach(fun({Package, Spec}) -> + Path = proplists:get_value(url, Spec, ""), + add_sources(Package, Path) + end, modules_spec(sources_dir(), "*")). + +available() -> + Jungle = modules_spec(sources_dir(), "*/*"), + Standalone = modules_spec(sources_dir(), "*"), + lists:keysort(1, + lists:foldl(fun({Key, Val}, Acc) -> + lists:keystore(Key, 1, Acc, {Key, Val}) + end, Jungle, Standalone)). +available(Module) when is_atom(Module) -> + available(jlib:atom_to_binary(Module)); +available(Package) when is_binary(Package) -> + Available = [jlib:atom_to_binary(K) || K<-proplists:get_keys(available())], + lists:member(Package, Available). + +available_command() -> + [short_spec(Item) || Item <- available()]. + +installed() -> + modules_spec(modules_dir(), "*"). +installed(Module) when is_atom(Module) -> + installed(jlib:atom_to_binary(Module)); +installed(Package) when is_binary(Package) -> + Installed = [jlib:atom_to_binary(K) || K<-proplists:get_keys(installed())], + lists:member(Package, Installed). + +installed_command() -> + [short_spec(Item) || Item <- installed()]. + +install(Module) when is_atom(Module) -> + install(jlib:atom_to_binary(Module)); +install(Package) when is_binary(Package) -> + Spec = [S || {Mod, S} <- available(), jlib:atom_to_binary(Mod)==Package], + case {Spec, installed(Package), is_contrib_allowed()} of + {_, _, false} -> + {error, not_allowed}; + {[], _, _} -> + {error, not_available}; + {_, true, _} -> + {error, conflict}; + {[Attrs], _, _} -> + Module = jlib:binary_to_atom(Package), + case compile_and_install(Module, Attrs) of + ok -> + code:add_patha(module_ebin_dir(Module)), + ok; + Error -> + delete_path(module_lib_dir(Module)), + Error + end + end. + +uninstall(Module) when is_atom(Module) -> + uninstall(jlib:atom_to_binary(Module)); +uninstall(Package) when is_binary(Package) -> + case installed(Package) of + true -> + Module = jlib:binary_to_atom(Package), + [catch gen_mod:stop_module(Host, Module) + || Host <- ejabberd_config:get_myhosts()], + code:purge(Module), + code:delete(Module), + code:del_path(module_ebin_dir(Module)), + delete_path(module_lib_dir(Module)); + false -> + {error, not_installed} + end. + +upgrade() -> + [{Package, upgrade(Package)} || {Package, _Spec} <- installed()]. +upgrade(Module) when is_atom(Module) -> + upgrade(jlib:atom_to_binary(Module)); +upgrade(Package) when is_binary(Package) -> + uninstall(Package), + install(Package). + +add_sources(Path) when is_list(Path) -> + add_sources(iolist_to_binary(module_name(Path)), Path). +add_sources(_, "") -> + {error, no_url}; +add_sources(Module, Path) when is_atom(Module), is_list(Path) -> + add_sources(jlib:atom_to_binary(Module), Path); +add_sources(Package, Path) when is_binary(Package), is_list(Path) -> + DestDir = sources_dir(), + RepDir = filename:join(DestDir, module_name(Path)), + delete_path(RepDir), + case filelib:ensure_dir(RepDir) of + ok -> + case {string:left(Path, 4), string:right(Path, 2)} of + {"http", "ip"} -> extract(zip, geturl(Path), DestDir); + {"http", "gz"} -> extract(tar, geturl(Path), DestDir); + {"http", _} -> extract_url(Path, DestDir); + {"git@", _} -> extract_github_master(Path, DestDir); + {_, "ip"} -> extract(zip, Path, DestDir); + {_, "gz"} -> extract(tar, Path, DestDir); + _ -> {error, unsupported_source} + end; + Error -> + Error + end. + +del_sources(Module) when is_atom(Module) -> + del_sources(jlib:atom_to_binary(Module)); +del_sources(Package) when is_binary(Package) -> + case uninstall(Package) of + ok -> + SrcDir = module_src_dir(jlib:binary_to_atom(Package)), + delete_path(SrcDir); + Error -> + Error + end. + +check(Module) when is_atom(Module) -> + check(jlib:atom_to_binary(Module)); +check(Package) when is_binary(Package) -> + case {available(Package), installed(Package)} of + {false, _} -> + {error, not_available}; + {_, false} -> + Status = install(Package), + uninstall(Package), + case Status of + ok -> check_sources(jlib:binary_to_atom(Package)); + Error -> Error + end; + _ -> + check_sources(jlib:binary_to_atom(Package)) + end. + +%% -- archives and variables functions + +geturl(Url) -> + geturl(Url, []). +geturl(Url, UsrOpts) -> + geturl(Url, [], UsrOpts). +geturl(Url, Hdrs, UsrOpts) -> + Host = case getenv("PROXY_SERVER", "", ":") of + [H, Port] -> [{proxy_host, H}, {proxy_port, list_to_integer(Port)}]; + [H] -> [{proxy_host, H}, {proxy_port, 8080}]; + _ -> [] + end, + User = case getenv("PROXY_USER", "", [4]) of + [U, Pass] -> [{proxy_user, U}, {proxy_password, Pass}]; + _ -> [] + end, + case httpc:request(get, {Url, Hdrs}, Host++User++UsrOpts, []) of + {ok, {{_, 200, _}, Headers, Response}} -> + {ok, Headers, Response}; + {ok, {{_, Code, _}, _Headers, Response}} -> + {error, {Code, Response}}; + {error, Reason} -> + {error, Reason} + end. + +getenv(Env) -> + getenv(Env, ""). +getenv(Env, Default) -> + case os:getenv(Env) of + false -> Default; + "" -> Default; + Value -> Value + end. +getenv(Env, Default, Separator) -> + string:tokens(getenv(Env, Default), Separator). + +extract(zip, {ok, _, Body}, DestDir) -> + extract(zip, iolist_to_binary(Body), DestDir); +extract(tar, {ok, _, Body}, DestDir) -> + extract(tar, {binary, iolist_to_binary(Body)}, DestDir); +extract(_, {error, Reason}, _) -> + {error, Reason}; +extract(zip, Zip, DestDir) -> + case zip:extract(Zip, [{cwd, DestDir}]) of + {ok, _} -> ok; + Error -> Error + end; +extract(tar, Tar, DestDir) -> + erl_tar:extract(Tar, [compressed, {cwd, DestDir}]). + +extract_url(Path, DestDir) -> + hd([extract_github_master(Path, DestDir) || string:str(Path, "github") > 0] + ++[{error, unsupported_source}]). + +extract_github_master(Repos, DestDir) -> + case extract(zip, geturl(Repos ++ "/archive/master.zip"), DestDir) of + ok -> + RepDir = filename:join(DestDir, module_name(Repos)), + file:rename(RepDir++"-master", RepDir); + Error -> + Error + end. + +copy_file(From, To) -> + filelib:ensure_dir(To), + file:copy(From, To). + +delete_path(Path) -> + case filelib:is_dir(Path) of + true -> + [delete_path(SubPath) || SubPath <- filelib:wildcard(Path++"/*")], + file:del_dir(Path); + false -> + file:delete(Path) + end. + +modules_dir() -> + DefaultDir = filename:join(getenv("HOME"), ".ejabberd-modules"), + getenv("CONTRIB_MODULES_PATH", DefaultDir). + +sources_dir() -> + filename:join(modules_dir(), "sources"). + +module_lib_dir(Package) -> + filename:join(modules_dir(), Package). + +module_ebin_dir(Package) -> + filename:join(module_lib_dir(Package), "ebin"). + +module_src_dir(Package) -> + Rep = module_name(Package), + SrcDir = sources_dir(), + Standalone = filelib:wildcard(Rep, SrcDir), + Jungle = filelib:wildcard("*/"++Rep, SrcDir), + case Standalone++Jungle of + [RepDir|_] -> filename:join(SrcDir, RepDir); + _ -> filename:join(SrcDir, Rep) + end. + +module_name(Id) -> + filename:basename(filename:rootname(Id)). + +module(Id) -> + jlib:binary_to_atom(iolist_to_binary(module_name(Id))). + +module_spec(Spec) -> + [{path, filename:dirname(Spec)} + | case consult(Spec) of + {ok, Meta} -> Meta; + _ -> [] + end]. + +modules_spec(Dir, Path) -> + Wildcard = filename:join(Path, "*.spec"), + lists:sort( + [{module(Match), module_spec(filename:join(Dir, Match))} + || Match <- filelib:wildcard(Wildcard, Dir)]). + +short_spec({Module, Attrs}) when is_atom(Module), is_list(Attrs) -> + {Module, proplists:get_value(summary, Attrs, "")}. + +is_contrib_allowed() -> + ejabberd_config:get_option(allow_contrib_modules, + fun(false) -> false; + (no) -> false; + (_) -> true + end, true). + +%% -- build functions + +check_sources(Module) -> + SrcDir = module_src_dir(Module), + SpecFile = filename:flatten([Module, ".spec"]), + {ok, Dir} = file:get_cwd(), + file:set_cwd(SrcDir), + HaveSrc = case filelib:is_dir("src") or filelib:is_dir("lib") of + true -> []; + false -> [{missing, "src (Erlang) or lib (Elixir) sources directory"}] + end, + DirCheck = lists:foldl( + fun({Type, Name}, Acc) -> + case filelib:Type(Name) of + true -> Acc; + false -> [{missing, Name}|Acc] + end + end, HaveSrc, [{is_file, "README.txt"}, + {is_file, "COPYING"}, + {is_file, SpecFile}]), + SpecCheck = case consult(SpecFile) of + {ok, Spec} -> + lists:foldl( + fun(Key, Acc) -> + case lists:keysearch(Key, 1, Spec) of + false -> [{missing_meta, Key}|Acc]; + {value, {Key, [_NoEmpty|_]}} -> Acc; + {value, {Key, Val}} -> [{invalid_meta, {Key, Val}}|Acc] + end + end, [], [author, summary, home, url]); + {error, Error} -> + [{invalid_spec, Error}] + end, + file:set_cwd(Dir), + Result = DirCheck ++ SpecCheck, + case Result of + [] -> ok; + _ -> {error, Result} + end. + +compile_and_install(Module, Spec) -> + SrcDir = module_src_dir(Module), + LibDir = module_lib_dir(Module), + case filelib:is_dir(SrcDir) of + true -> + {ok, Dir} = file:get_cwd(), + file:set_cwd(SrcDir), + Result = case compile(Module, Spec, LibDir) of + ok -> install(Module, Spec, LibDir); + Error -> Error + end, + file:set_cwd(Dir), + Result; + false -> + Path = proplists:get_value(url, Spec, ""), + case add_sources(Module, Path) of + ok -> compile_and_install(Module, Spec); + Error -> Error + end + end. + +compile(_Module, _Spec, DestDir) -> + Ebin = filename:join(DestDir, "ebin"), + filelib:ensure_dir(filename:join(Ebin, ".")), + EjabBin = filename:dirname(code:which(ejabberd)), + EjabInc = filename:join(filename:dirname(EjabBin), "include"), + Logger = case code:is_loaded(lager) of + {file, _} -> [{d, 'LAGER'}]; + _ -> [] + end, + Options = [{outdir, Ebin}, {i, "include"}, {i, EjabInc}, + {d, 'NO_EXT_LIB'}, %% use include instead of include_lib + verbose, report_errors, report_warnings] + ++ Logger, + Result = [case compile:file(File, Options) of + {ok, _} -> ok; + {ok, _, _} -> ok; + {ok, _, _, _} -> ok; + error -> {error, {compilation_failed, File}}; + Error -> Error + end + || File <- filelib:wildcard("src/*.erl")], + case lists:dropwhile( + fun(ok) -> true; + (_) -> false + end, Result) of + [] -> ok; + [Error|_] -> Error + end. + +install(Module, _Spec, DestDir) -> + SpecFile = filename:flatten([Module, ".spec"]), + [copy_file(File, filename:join(DestDir, File)) + || File <- [SpecFile | filelib:wildcard("{ebin,priv,conf,include}/**")]], + ok. + +%% -- YAML spec parser + +consult(File) -> + case p1_yaml:decode_from_file(File, [plain_as_atom]) of + {ok, []} -> {ok, []}; + {ok, [Doc|_]} -> {ok, [format(Spec) || Spec <- Doc]}; + {error, Err} -> {error, p1_yaml:format_error(Err)} + end. + +format({Key, Val}) when is_binary(Val) -> + {Key, binary_to_list(Val)}; +format({Key, Val}) -> % TODO: improve Yaml parsing + {Key, Val}. diff --git a/src/gen_mod.erl b/src/gen_mod.erl index 645e3142d..e628b06ab 100644 --- a/src/gen_mod.erl +++ b/src/gen_mod.erl @@ -33,7 +33,7 @@ get_opt_host/3, db_type/1, db_type/2, get_module_opt/5, get_module_opt_host/3, loaded_modules/1, loaded_modules_with_opts/1, get_hosts/2, - get_module_proc/2, is_loaded/2]). + get_module_proc/2, is_loaded/2, default_db/1]). %%-export([behaviour_info/1]). @@ -212,24 +212,36 @@ get_opt_host(Host, Opts, Default) -> -spec db_type(opts()) -> odbc | mnesia | riak. db_type(Opts) -> - get_opt(db_type, Opts, - fun(odbc) -> odbc; - (internal) -> mnesia; - (mnesia) -> mnesia; - (riak) -> riak - end, - mnesia). + db_type(global, Opts). --spec db_type(binary(), atom()) -> odbc | mnesia | riak. +-spec db_type(binary() | global, atom() | opts()) -> odbc | mnesia | riak. -db_type(Host, Module) -> +db_type(Host, Module) when is_atom(Module) -> get_module_opt(Host, Module, db_type, fun(odbc) -> odbc; (internal) -> mnesia; (mnesia) -> mnesia; (riak) -> riak end, - mnesia). + default_db(Host)); +db_type(Host, Opts) when is_list(Opts) -> + get_opt(db_type, Opts, + fun(odbc) -> odbc; + (internal) -> mnesia; + (mnesia) -> mnesia; + (riak) -> riak + end, + default_db(Host)). + +-spec default_db(binary() | global) -> odbc | mnesia | riak. + +default_db(Host) -> + ejabberd_config:get_option({default_db, Host}, + fun(odbc) -> odbc; + (mnesia) -> mnesia; + (riak) -> riak; + (internal) -> mnesia + end, mnesia). -spec loaded_modules(binary()) -> [atom()]. diff --git a/src/jlib.erl b/src/jlib.erl index 2c0f30b3f..76886a7dc 100644 --- a/src/jlib.erl +++ b/src/jlib.erl @@ -57,6 +57,7 @@ %% TODO: Remove once XEP-0091 is Obsolete %% TODO: Remove once XEP-0091 is Obsolete +-include("ejabberd.hrl"). -include("jlib.hrl"). -export_type([jid/0]). @@ -972,7 +973,7 @@ i2l(L, N) when is_binary(L) -> _ -> i2l(<<$0, L/binary>>, N) end. --spec queue_drop_while(fun((term()) -> boolean()), queue()) -> queue(). +-spec queue_drop_while(fun((term()) -> boolean()), ?TQUEUE) -> ?TQUEUE. queue_drop_while(F, Q) -> case queue:peek(Q) of diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl new file mode 100644 index 000000000..0162a40c3 --- /dev/null +++ b/src/mod_admin_extra.erl @@ -0,0 +1,1583 @@ +%%%------------------------------------------------------------------- +%%% File : mod_admin_extra.erl +%%% Author : Badlop <badlop@process-one.net> +%%% Purpose : Contributed administrative functions and commands +%%% Created : 10 Aug 2008 by Badlop <badlop@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2008 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%------------------------------------------------------------------- + +-module(mod_admin_extra). +-author('badlop@process-one.net'). + +-behaviour(gen_mod). + +-include("logger.hrl"). + +-export([start/2, stop/1, + %% Node + compile/1, + load_config/1, + get_cookie/0, + remove_node/1, + export2odbc/2, + %% Accounts + set_password/3, + check_password_hash/4, + delete_old_users/1, + delete_old_users_vhost/2, + ban_account/3, + num_active_users/2, + %% Sessions + num_resources/2, + resource_num/3, + kick_session/4, + status_num/2, status_num/1, + status_list/2, status_list/1, + connected_users_info/0, + connected_users_vhost/1, + set_presence/7, + user_sessions_info/2, + %% Vcard + set_nickname/3, + get_vcard/3, + get_vcard/4, + get_vcard_multi/4, + set_vcard/4, + set_vcard/5, + %% Roster + add_rosteritem/7, + delete_rosteritem/4, + process_rosteritems/5, + get_roster/2, + push_roster/3, + push_roster_all/1, + push_alltoall/2, + %% mod_last + get_last/2, + set_last/4, + %% mod_private + private_get/4, + private_set/3, + %% mod_shared_roster + srg_create/5, + srg_delete/2, + srg_list/1, + srg_get_info/2, + srg_get_members/2, + srg_user_add/4, + srg_user_del/4, + %% Stanza + send_message/5, + send_stanza_c2s/4, + privacy_set/3, + %% Stats + stats/1, stats/2 + ]). + +-include("ejabberd.hrl"). +-include("ejabberd_commands.hrl"). +-include("mod_roster.hrl"). +-include("jlib.hrl"). + +%% Copied from ejabberd_sm.erl +-record(session, {sid, usr, us, priority, info}). + + +%%% +%%% gen_mod +%%% + +start(_Host, _Opts) -> + ejabberd_commands:register_commands(commands()). + +stop(_Host) -> + ejabberd_commands:unregister_commands(commands()). + + +%%% +%%% Register commands +%%% + +commands() -> + Vcard1FieldsString = "Some vcard field names in get/set_vcard are:\n" + " FN - Full Name\n" + " NICKNAME - Nickname\n" + " BDAY - Birthday\n" + " TITLE - Work: Position\n" + " ROLE - Work: Role", + + Vcard2FieldsString = "Some vcard field names and subnames in get/set_vcard2 are:\n" + " N FAMILY - Family name\n" + " N GIVEN - Given name\n" + " N MIDDLE - Middle name\n" + " ADR CTRY - Address: Country\n" + " ADR LOCALITY - Address: City\n" + " TEL HOME - Telephone: Home\n" + " TEL CELL - Telephone: Cellphone\n" + " TEL WORK - Telephone: Work\n" + " TEL VOICE - Telephone: Voice\n" + " EMAIL USERID - E-Mail Address\n" + " ORG ORGNAME - Work: Company\n" + " ORG ORGUNIT - Work: Department", + + VcardXEP = "For a full list of vCard fields check XEP-0054: vcard-temp at " + "http://www.xmpp.org/extensions/xep-0054.html", + + [ + #ejabberd_commands{name = compile, tags = [erlang], + desc = "Recompile and reload Erlang source code file", + module = ?MODULE, function = compile, + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = load_config, tags = [server], + desc = "Load ejabberd configuration file", + module = ?MODULE, function = load_config, + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = get_cookie, tags = [erlang], + desc = "Get the Erlang cookie of this node", + module = ?MODULE, function = get_cookie, + args = [], + result = {cookie, string}}, + #ejabberd_commands{name = remove_node, tags = [erlang], + desc = "Remove an ejabberd node from Mnesia clustering config", + module = ?MODULE, function = remove_node, + args = [{node, string}], + result = {res, rescode}}, + #ejabberd_commands{name = export2odbc, tags = [mnesia], %% Copied to ejabberd 2.1.x after 11 + desc = "Export Mnesia tables to files in directory", + module = ?MODULE, function = export2odbc, + args = [{host, string}, {path, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = num_active_users, tags = [accounts, stats], + desc = "Get number of users active in the last days", + module = ?MODULE, function = num_active_users, + args = [{host, binary}, {days, integer}], + result = {users, integer}}, + #ejabberd_commands{name = delete_old_users, tags = [accounts, purge], + desc = "Delete users that didn't log in last days, or that never logged", + module = ?MODULE, function = delete_old_users, + args = [{days, integer}], + result = {res, restuple}}, + #ejabberd_commands{name = delete_old_users_vhost, tags = [accounts, purge], + desc = "Delete users that didn't log in last days in vhost, or that never logged", + module = ?MODULE, function = delete_old_users_vhost, + args = [{host, binary}, {days, integer}], + result = {res, restuple}}, + + #ejabberd_commands{name = check_account, tags = [accounts], + desc = "Check if an account exists or not", + module = ejabberd_auth, function = is_user_exists, + args = [{user, binary}, {host, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = check_password, tags = [accounts], + desc = "Check if a password is correct", + module = ejabberd_auth, function = check_password, + args = [{user, binary}, {host, binary}, {password, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = check_password_hash, tags = [accounts], + desc = "Check if the password hash is correct", + longdesc = "Allowed hash methods: md5, sha.", + module = ?MODULE, function = check_password_hash, + args = [{user, binary}, {host, binary}, {passwordhash, binary}, {hashmethod, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = change_password, tags = [accounts], + desc = "Change the password of an account", + module = ?MODULE, function = set_password, + args = [{user, binary}, {host, binary}, {newpass, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = ban_account, tags = [accounts], + desc = "Ban an account: kick sessions and set random password", + module = ?MODULE, function = ban_account, + args = [{user, binary}, {host, binary}, {reason, binary}], + result = {res, rescode}}, + + #ejabberd_commands{name = num_resources, tags = [session], + desc = "Get the number of resources of a user", + module = ?MODULE, function = num_resources, + args = [{user, binary}, {host, binary}], + result = {resources, integer}}, + #ejabberd_commands{name = resource_num, tags = [session], + desc = "Resource string of a session number", + module = ?MODULE, function = resource_num, + args = [{user, binary}, {host, binary}, {num, integer}], + result = {resource, string}}, + #ejabberd_commands{name = kick_session, tags = [session], + desc = "Kick a user session", + module = ?MODULE, function = kick_session, + args = [{user, binary}, {host, binary}, {resource, binary}, {reason, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = status_num_host, tags = [session, stats], + desc = "Number of logged users with this status in host", + module = ?MODULE, function = status_num, + args = [{host, binary}, {status, binary}], + result = {users, integer}}, + #ejabberd_commands{name = status_num, tags = [session, stats], + desc = "Number of logged users with this status", + module = ?MODULE, function = status_num, + args = [{status, binary}], + result = {users, integer}}, + #ejabberd_commands{name = status_list_host, tags = [session], + desc = "List of users logged in host with their statuses", + module = ?MODULE, function = status_list, + args = [{host, binary}, {status, binary}], + result = {users, {list, + {userstatus, {tuple, [ + {user, string}, + {host, string}, + {resource, string}, + {priority, integer}, + {status, string} + ]}} + }}}, + #ejabberd_commands{name = status_list, tags = [session], + desc = "List of logged users with this status", + module = ?MODULE, function = status_list, + args = [{status, binary}], + result = {users, {list, + {userstatus, {tuple, [ + {user, string}, + {host, string}, + {resource, string}, + {priority, integer}, + {status, string} + ]}} + }}}, + #ejabberd_commands{name = connected_users_info, + tags = [session], + desc = "List all established sessions and their information", + module = ?MODULE, function = connected_users_info, + args = [], + result = {connected_users_info, + {list, + {sessions, {tuple, + [{jid, string}, + {connection, string}, + {ip, string}, + {port, integer}, + {priority, integer}, + {node, string}, + {uptime, integer} + ]}} + }}}, + #ejabberd_commands{name = connected_users_vhost, + tags = [session], + desc = "Get the list of established sessions in a vhost", + module = ?MODULE, function = connected_users_vhost, + args = [{host, string}], + result = {connected_users_vhost, {list, {sessions, string}}}}, + #ejabberd_commands{name = user_sessions_info, + tags = [session], + desc = "Get information about all sessions of a user", + module = ?MODULE, function = user_sessions_info, + args = [{user, binary}, {host, binary}], + result = {sessions_info, + {list, + {session, {tuple, + [{connection, string}, + {ip, string}, + {port, integer}, + {priority, integer}, + {node, string}, + {uptime, integer}, + {status, string}, + {resource, string}, + {statustext, string} + ]}} + }}}, + + #ejabberd_commands{name = set_presence, + tags = [session], + desc = "Set presence of a session", + module = ?MODULE, function = set_presence, + args = [{user, binary}, {host, binary}, + {resource, binary}, {type, binary}, + {show, binary}, {status, binary}, + {priority, binary}], + result = {res, rescode}}, + + #ejabberd_commands{name = set_nickname, tags = [vcard], + desc = "Set nickname in a user's vCard", + module = ?MODULE, function = set_nickname, + args = [{user, binary}, {host, binary}, {nickname, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = get_vcard, tags = [vcard], + desc = "Get content from a vCard field", + longdesc = Vcard1FieldsString ++ "\n" ++ Vcard2FieldsString ++ "\n\n" ++ VcardXEP, + module = ?MODULE, function = get_vcard, + args = [{user, binary}, {host, binary}, {name, binary}], + result = {content, string}}, + #ejabberd_commands{name = get_vcard2, tags = [vcard], + desc = "Get content from a vCard field", + longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = get_vcard, + args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}], + result = {content, string}}, + #ejabberd_commands{name = get_vcard2_multi, tags = [vcard], + desc = "Get multiple contents from a vCard field", + longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = get_vcard_multi, + args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}], + result = {contents, {list, {value, string}}}}, + + #ejabberd_commands{name = set_vcard, tags = [vcard], + desc = "Set content in a vCard field", + longdesc = Vcard1FieldsString ++ "\n" ++ Vcard2FieldsString ++ "\n\n" ++ VcardXEP, + module = ?MODULE, function = set_vcard, + args = [{user, binary}, {host, binary}, {name, binary}, {content, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = set_vcard2, tags = [vcard], + desc = "Set content in a vCard subfield", + longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = set_vcard, + args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {content, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = set_vcard2_multi, tags = [vcard], + desc = "Set multiple contents in a vCard subfield", + longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = set_vcard, + args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {contents, {list, binary}}], + result = {res, rescode}}, + + #ejabberd_commands{name = add_rosteritem, tags = [roster], + desc = "Add an item to a user's roster (supports ODBC)", + module = ?MODULE, function = add_rosteritem, + args = [{localuser, binary}, {localserver, binary}, + {user, binary}, {server, binary}, + {nick, binary}, {group, binary}, + {subs, binary}], + result = {res, rescode}}, + %%{"", "subs= none, from, to or both"}, + %%{"", "example: add-roster peter localhost mike server.com MiKe Employees both"}, + %%{"", "will add mike@server.com to peter@localhost roster"}, + #ejabberd_commands{name = delete_rosteritem, tags = [roster], + desc = "Delete an item from a user's roster (supports ODBC)", + module = ?MODULE, function = delete_rosteritem, + args = [{localuser, binary}, {localserver, binary}, + {user, binary}, {server, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = process_rosteritems, tags = [roster], + desc = "List or delete rosteritems that match filtering options", + longdesc = "Explanation of each argument:\n" + " - action: what to do with each rosteritem that " + "matches all the filtering options\n" + " - subs: subscription type\n" + " - asks: pending subscription\n" + " - users: the JIDs of the local user\n" + " - contacts: the JIDs of the contact in the roster\n" + "\n" + "Allowed values in the arguments:\n" + " ACTION = list | delete\n" + " SUBS = SUB[:SUB]* | any\n" + " SUB = none | from | to | both\n" + " ASKS = ASK[:ASK]* | any\n" + " ASK = none | out | in\n" + " USERS = JID[:JID]* | any\n" + " CONTACTS = JID[:JID]* | any\n" + " JID = characters valid in a JID, and can use the " + "globs: *, ?, ! and [...]\n" + "\n" + "This example will list roster items with subscription " + "'none', 'from' or 'to' that have any ask property, of " + "local users which JID is in the virtual host " + "'example.org' and that the contact JID is either a " + "bare server name (without user part) or that has a " + "user part and the server part contains the word 'icq'" + ":\n list none:from:to any *@example.org *:*@*icq*", + module = ?MODULE, function = process_rosteritems, + args = [{action, string}, {subs, string}, + {asks, string}, {users, string}, + {contacts, string}], + result = {response, + {list, + {pairs, {tuple, + [{user, string}, + {contact, string} + ]}} + }}}, + #ejabberd_commands{name = get_roster, tags = [roster], + desc = "Get roster of a local user", + module = ?MODULE, function = get_roster, + args = [{user, binary}, {host, binary}], + result = {contacts, {list, {contact, {tuple, [ + {jid, string}, + {nick, string}, + {subscription, string}, + {ask, string}, + {group, string} + ]}}}}}, + #ejabberd_commands{name = push_roster, tags = [roster], + desc = "Push template roster from file to a user", + module = ?MODULE, function = push_roster, + args = [{file, string}, {user, string}, {host, string}], + result = {res, rescode}}, + #ejabberd_commands{name = push_roster_all, tags = [roster], + desc = "Push template roster from file to all those users", + module = ?MODULE, function = push_roster_all, + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = push_alltoall, tags = [roster], + desc = "Add all the users to all the users of Host in Group", + module = ?MODULE, function = push_alltoall, + args = [{host, string}, {group, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = get_last, tags = [last], + desc = "Get last activity information", + longdesc = "Timestamp is the seconds since" + "1970-01-01 00:00:00 UTC, for example: date +%s", + module = ?MODULE, function = get_last, + args = [{user, binary}, {host, binary}], + result = {last_activity, string}}, + #ejabberd_commands{name = set_last, tags = [last], + desc = "Set last activity information", + longdesc = "Timestamp is the seconds since" + "1970-01-01 00:00:00 UTC, for example: date +%s", + module = ?MODULE, function = set_last, + args = [{user, string}, {host, string}, {timestamp, integer}, {status, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = private_get, tags = [private], + desc = "Get some information from a user private storage", + module = ?MODULE, function = private_get, + args = [{user, binary}, {host, binary}, {element, binary}, {ns, binary}], + result = {res, string}}, + #ejabberd_commands{name = private_set, tags = [private], + desc = "Set to the user private storage", + module = ?MODULE, function = private_set, + args = [{user, binary}, {host, binary}, {element, binary}], + result = {res, rescode}}, + + #ejabberd_commands{name = srg_create, tags = [shared_roster_group], + desc = "Create a Shared Roster Group", + longdesc = "If you want to specify several group " + "identifiers in the Display argument,\n" + "put \\ \" around the argument and\nseparate the " + "identifiers with \\ \\ n\n" + "For example:\n" + " ejabberdctl srg_create group3 localhost " + "name desc \\\"group1\\\\ngroup2\\\"", + module = ?MODULE, function = srg_create, + args = [{group, binary}, {host, binary}, + {name, binary}, {description, binary}, {display, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = srg_delete, tags = [shared_roster_group], + desc = "Delete a Shared Roster Group", + module = ?MODULE, function = srg_delete, + args = [{group, binary}, {host, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = srg_list, tags = [shared_roster_group], + desc = "List the Shared Roster Groups in Host", + module = ?MODULE, function = srg_list, + args = [{host, binary}], + result = {groups, {list, {id, string}}}}, + #ejabberd_commands{name = srg_get_info, tags = [shared_roster_group], + desc = "Get info of a Shared Roster Group", + module = ?MODULE, function = srg_get_info, + args = [{group, binary}, {host, binary}], + result = {informations, {list, {information, {tuple, [{key, string}, {value, string}]}}}}}, + #ejabberd_commands{name = srg_get_members, tags = [shared_roster_group], + desc = "Get members of a Shared Roster Group", + module = ?MODULE, function = srg_get_members, + args = [{group, binary}, {host, binary}], + result = {members, {list, {member, string}}}}, + #ejabberd_commands{name = srg_user_add, tags = [shared_roster_group], + desc = "Add the JID user@host to the Shared Roster Group", + module = ?MODULE, function = srg_user_add, + args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = srg_user_del, tags = [shared_roster_group], + desc = "Delete this JID user@host from the Shared Roster Group", + module = ?MODULE, function = srg_user_del, + args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}], + result = {res, rescode}}, + + #ejabberd_commands{name = send_message, tags = [stanza], + desc = "Send a message to a local or remote bare of full JID", + module = ?MODULE, function = send_message, + args = [{type, binary}, {from, binary}, {to, binary}, + {subject, binary}, {body, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = send_stanza_c2s, tags = [stanza], + desc = "Send a stanza as if sent from a c2s session", + module = ?MODULE, function = send_stanza_c2s, + args = [{user, binary}, {host, binary}, {resource, binary}, {stanza, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = privacy_set, tags = [stanza], + desc = "Send a IQ set privacy stanza for a local account", + module = ?MODULE, function = privacy_set, + args = [{user, binary}, {host, binary}, {xmlquery, binary}], + result = {res, rescode}}, + + #ejabberd_commands{name = stats, tags = [stats], + desc = "Get statistical value: registeredusers onlineusers onlineusersnode uptimeseconds", + module = ?MODULE, function = stats, + args = [{name, binary}], + result = {stat, integer}}, + #ejabberd_commands{name = stats_host, tags = [stats], + desc = "Get statistical value for this host: registeredusers onlineusers", + module = ?MODULE, function = stats, + args = [{name, binary}, {host, binary}], + result = {stat, integer}} + ]. + + +%%% +%%% Node +%%% + +compile(File) -> + compile:file(File). + +load_config(Path) -> + ok = ejabberd_config:load_file(Path). + +get_cookie() -> + atom_to_list(erlang:get_cookie()). + +remove_node(Node) -> + mnesia:del_table_copy(schema, list_to_atom(Node)), + ok. + +export2odbc(Host, Directory) -> + Tables = [ + {export_last, last}, + {export_offline, offline}, + {export_passwd, passwd}, + {export_private_storage, private_storage}, + {export_roster, roster}, + {export_vcard, vcard}, + {export_vcard_search, vcard_search}], + Export = fun({TableFun, Table}) -> + Filename = filename:join([Directory, atom_to_list(Table)++".txt"]), + io:format("Trying to export Mnesia table '~p' on Host '~s' to file '~s'~n", [Table, Host, Filename]), + Res = (catch ejd2odbc:TableFun(Host, Filename)), + io:format(" Result: ~p~n", [Res]) + end, + lists:foreach(Export, Tables), + ok. + + +%%% +%%% Accounts +%%% + +set_password(User, Host, Password) -> + case ejabberd_auth:set_password(User, Host, Password) of + ok -> + ok; + _ -> + error + end. + +%% Copied some code from ejabberd_commands.erl +check_password_hash(User, Host, PasswordHash, HashMethod) -> + AccountPass = ejabberd_auth:get_password_s(User, Host), + AccountPassHash = case HashMethod of + "md5" -> get_md5(AccountPass); + "sha" -> get_sha(AccountPass); + _ -> undefined + end, + case AccountPassHash of + undefined -> error; + PasswordHash -> ok; + _ -> error + end. +get_md5(AccountPass) -> + lists:flatten([io_lib:format("~.16B", [X]) + || X <- binary_to_list(erlang:md5(AccountPass))]). +get_sha(AccountPass) -> + lists:flatten([io_lib:format("~.16B", [X]) + || X <- binary_to_list(p1_sha:sha1(AccountPass))]). + +num_active_users(Host, Days) -> + list_last_activity(Host, true, Days). + +%% Code based on ejabberd/src/web/ejabberd_web_admin.erl +list_last_activity(Host, Integral, Days) -> + {MegaSecs, Secs, _MicroSecs} = now(), + TimeStamp = MegaSecs * 1000000 + Secs, + TS = TimeStamp - Days * 86400, + case catch mnesia:dirty_select( + last_activity, [{{last_activity, {'_', Host}, '$1', '_'}, + [{'>', '$1', TS}], + [{'trunc', {'/', + {'-', TimeStamp, '$1'}, + 86400}}]}]) of + {'EXIT', _Reason} -> + []; + Vals -> + Hist = histogram(Vals, Integral), + if + Hist == [] -> + 0; + true -> + Left = Days - length(Hist), + Tail = if + Integral -> + lists:duplicate(Left, lists:last(Hist)); + true -> + lists:duplicate(Left, 0) + end, + lists:nth(Days, Hist ++ Tail) + end + end. +histogram(Values, Integral) -> + histogram(lists:sort(Values), Integral, 0, 0, []). +histogram([H | T], Integral, Current, Count, Hist) when Current == H -> + histogram(T, Integral, Current, Count + 1, Hist); +histogram([H | _] = Values, Integral, Current, Count, Hist) when Current < H -> + if + Integral -> + histogram(Values, Integral, Current + 1, Count, [Count | Hist]); + true -> + histogram(Values, Integral, Current + 1, 0, [Count | Hist]) + end; +histogram([], _Integral, _Current, Count, Hist) -> + if + Count > 0 -> + lists:reverse([Count | Hist]); + true -> + lists:reverse(Hist) + end. + + +delete_old_users(Days) -> + %% Get the list of registered users + Users = ejabberd_auth:dirty_get_registered_users(), + + {removed, N, UR} = delete_old_users(Days, Users), + {ok, io_lib:format("Deleted ~p users: ~p", [N, UR])}. + +delete_old_users_vhost(Host, Days) -> + %% Get the list of registered users + Users = ejabberd_auth:get_vh_registered_users(Host), + + {removed, N, UR} = delete_old_users(Days, Users), + {ok, io_lib:format("Deleted ~p users: ~p", [N, UR])}. + +delete_old_users(Days, Users) -> + %% Convert older time + SecOlder = Days*24*60*60, + + %% Get current time + {MegaSecs, Secs, _MicroSecs} = now(), + TimeStamp_now = MegaSecs * 1000000 + Secs, + + %% For a user, remove if required and answer true + F = fun({LUser, LServer}) -> + %% Check if the user is logged + case ejabberd_sm:get_user_resources(LUser, LServer) of + %% If it isnt + [] -> + %% Look for his last_activity + case (get_lastactivity_module(LServer)):get_last_info(LUser, LServer) of + %% If it is + %% existent: + {ok, TimeStamp, _Status} -> + %% get his age + Sec = TimeStamp_now - TimeStamp, + %% If he is + if + %% younger than SecOlder: + Sec < SecOlder -> + %% do nothing + false; + %% older: + true -> + %% remove the user + ejabberd_auth:remove_user(LUser, LServer), + true + end; + %% nonexistent: + not_found -> + %% remove the user + ejabberd_auth:remove_user(LUser, LServer), + true + end; + %% Else + _ -> + %% do nothing + false + end + end, + %% Apply the function to every user in the list + Users_removed = lists:filter(F, Users), + {removed, length(Users_removed), Users_removed}. + +get_lastactivity_module(Server) -> + case lists:member(mod_last, gen_mod:loaded_modules(Server)) of + true -> mod_last; + _ -> mod_last_odbc + end. + + +%% +%% Ban account + +ban_account(User, Host, ReasonText) -> + Reason = prepare_reason(ReasonText), + kick_sessions(User, Host, Reason), + set_random_password(User, Host, Reason), + ok. + +kick_sessions(User, Server, Reason) -> + lists:map( + fun(Resource) -> + kick_this_session(User, Server, Resource, Reason) + end, + get_resources(User, Server)). + +get_resources(User, Server) -> + lists:map( + fun(Session) -> + element(3, Session#session.usr) + end, + get_sessions(User, Server)). + +get_sessions(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + Sessions = mnesia:dirty_index_read(session, {LUser, LServer}, #session.us), + true = is_list(Sessions), + Sessions. + +set_random_password(User, Server, Reason) -> + NewPass = build_random_password(Reason), + set_password_auth(User, Server, NewPass). + +build_random_password(Reason) -> + Date = jlib:timestamp_to_iso(calendar:universal_time()), + RandomString = randoms:get_string(), + <<"BANNED_ACCOUNT--", Date/binary, "--", RandomString/binary, "--", Reason/binary>>. + +set_password_auth(User, Server, Password) -> + ok = ejabberd_auth:set_password(User, Server, Password). + +prepare_reason([]) -> + <<"Kicked by administrator">>; +prepare_reason([Reason]) -> + Reason; +prepare_reason(Reason) when is_binary(Reason) -> + Reason. + +%%% +%%% Sessions +%%% + +num_resources(User, Host) -> + length(ejabberd_sm:get_user_resources(User, Host)). + +resource_num(User, Host, Num) -> + Resources = ejabberd_sm:get_user_resources(User, Host), + case (0<Num) and (Num=<length(Resources)) of + true -> + lists:nth(Num, Resources); + false -> + lists:flatten(io_lib:format("Error: Wrong resource number: ~p", [Num])) + end. + +kick_session(User, Server, Resource, ReasonText) -> + kick_this_session(User, Server, Resource, prepare_reason(ReasonText)), + ok. + +kick_this_session(User, Server, Resource, Reason) -> + ejabberd_sm:route(jlib:make_jid(<<"">>, <<"">>, <<"">>), + jlib:make_jid(User, Server, Resource), + {broadcast, {exit, Reason}}). + +status_num(Host, Status) -> + length(get_status_list(Host, Status)). +status_num(Status) -> + status_num(<<"all">>, Status). +status_list(Host, Status) -> + Res = get_status_list(Host, Status), + [{U, S, R, P, St} || {U, S, R, P, St} <- Res]. +status_list(Status) -> + status_list(<<"all">>, Status). + + +get_status_list(Host, Status_required) -> + %% Get list of all logged users + Sessions = ejabberd_sm:dirty_get_my_sessions_list(), + %% Reformat the list + Sessions2 = [ {Session#session.usr, Session#session.sid, Session#session.priority} || Session <- Sessions], + Fhost = case Host of + <<"all">> -> + %% All hosts are requested, so dont filter at all + fun(_, _) -> true end; + _ -> + %% Filter the list, only Host is interesting + fun(A, B) -> A == B end + end, + Sessions3 = [ {Pid, Server, Priority} || {{_User, Server, _Resource}, {_, Pid}, Priority} <- Sessions2, apply(Fhost, [Server, Host])], + %% For each Pid, get its presence + Sessions4 = [ {catch ejabberd_c2s:get_presence(Pid), Server, Priority} || {Pid, Server, Priority} <- Sessions3], + %% Filter by status + Fstatus = case Status_required of + <<"all">> -> + fun(_, _) -> true end; + _ -> + fun(A, B) -> A == B end + end, + [{User, Server, Resource, Priority, stringize(Status_text)} + || {{User, Resource, Status, Status_text}, Server, Priority} <- Sessions4, + apply(Fstatus, [Status, Status_required])]. + +connected_users_info() -> + USRIs = dirty_get_sessions_list2(), + CurrentSec = calendar:datetime_to_gregorian_seconds({date(), time()}), + lists:map( + fun([{U, S, R}, {Now, Pid}, Priority, Info]) -> + Conn = proplists:get_value(conn, Info), + {Ip, Port} = proplists:get_value(ip, Info), + IPS = inet_parse:ntoa(Ip), + NodeS = atom_to_list(node(Pid)), + Uptime = CurrentSec - calendar:datetime_to_gregorian_seconds( + calendar:now_to_local_time(Now)), + PriorityI = case Priority of + PI when is_integer(PI) -> PI; + _ -> nil + end, + {[U, $@, S, $/, R], atom_to_list(Conn), IPS, Port, PriorityI, NodeS, Uptime} + end, + USRIs). + +connected_users_vhost(Host) -> + USRs = ejabberd_sm:get_vh_session_list(Host), + [ [U, $@, S, $/, R] || {U, S, R} <- USRs]. + +%% Code copied from ejabberd_sm.erl and customized +dirty_get_sessions_list2() -> + mnesia:dirty_select( + session, + [{#session{usr = '$1', sid = '$2', priority = '$3', info = '$4', _ = '_'}, + [], + [['$1', '$2', '$3', '$4']]}]). + +%% Make string more print-friendly +stringize(String) -> + %% Replace newline characters with other code + ejabberd_regexp:greplace(String, <<"\n">>, <<"\\n">>). + +set_presence(User, Host, Resource, Type, Show, Status, Priority) -> + Pid = ejabberd_sm:get_session_pid(User, Host, Resource), + USR = jlib:jid_to_string(jlib:make_jid(User, Host, Resource)), + US = jlib:jid_to_string(jlib:make_jid(User, Host, <<>>)), + Message = {route_xmlstreamelement, + {xmlel, <<"presence">>, + [{<<"from">>, USR}, {<<"to">>, US}, {<<"type">>, Type}], + [{xmlel, <<"show">>, [], [{xmlcdata, Show}]}, + {xmlel, <<"status">>, [], [{xmlcdata, Status}]}, + {xmlel, <<"priority">>, [], [{xmlcdata, Priority}]}]}}, + Pid ! Message. + +user_sessions_info(User, Host) -> + CurrentSec = calendar:datetime_to_gregorian_seconds({date(), time()}), + US = {User, Host}, + Sessions = case catch mnesia:dirty_index_read(session, US, #session.us) of + {'EXIT', _Reason} -> + []; + Ss -> + Ss + end, + lists:map( + fun(Session) -> + {_U, _S, Resource} = Session#session.usr, + {Now, Pid} = Session#session.sid, + {_U, _Resource, Status, StatusText} = ejabberd_c2s:get_presence(Pid), + Info = Session#session.info, + Priority = Session#session.priority, + Conn = proplists:get_value(conn, Info), + {Ip, Port} = proplists:get_value(ip, Info), + IPS = inet_parse:ntoa(Ip), + NodeS = atom_to_list(node(Pid)), + Uptime = CurrentSec - calendar:datetime_to_gregorian_seconds( + calendar:now_to_local_time(Now)), + {atom_to_list(Conn), IPS, Port, Priority, NodeS, Uptime, Status, Resource, StatusText} + end, + Sessions). + + +%%% +%%% Vcard +%%% + +set_nickname(User, Host, Nickname) -> + R = mod_vcard:process_sm_iq( + {jid, User, Host, <<>>, User, Host, <<>>}, + {jid, User, Host, <<>>, User, Host, <<>>}, + {iq, <<>>, set, <<>>, <<"en">>, + {xmlel, <<"vCard">>, [ + {<<"xmlns">>, <<"vcard-temp">>}], [ + {xmlel, <<"NICKNAME">>, [], [{xmlcdata, Nickname}]} + ] + }}), + case R of + {iq, <<>>, result, <<>>, _L, []} -> + ok; + _ -> + error + end. + +get_vcard(User, Host, Name) -> + [Res | _] = get_vcard_content(User, Host, [Name]), + Res. + +get_vcard(User, Host, Name, Subname) -> + [Res | _] = get_vcard_content(User, Host, [Name, Subname]), + Res. + +get_vcard_multi(User, Host, Name, Subname) -> + get_vcard_content(User, Host, [Name, Subname]). + +set_vcard(User, Host, Name, SomeContent) -> + set_vcard_content(User, Host, [Name], SomeContent). + +set_vcard(User, Host, Name, Subname, SomeContent) -> + set_vcard_content(User, Host, [Name, Subname], SomeContent). + + +%% +%% Internal vcard + +get_module_resource(Server) -> + case gen_mod:get_module_opt(Server, ?MODULE, module_resource, fun(A) -> A end, none) of + none -> list_to_binary(atom_to_list(?MODULE)); + R when is_binary(R) -> R + end. + +get_vcard_content(User, Server, Data) -> + [{_, Module, Function, _Opts}] = ets:lookup(sm_iqtable, {?NS_VCARD, Server}), + JID = jlib:make_jid(User, Server, get_module_resource(Server)), + IQ = #iq{type = get, xmlns = ?NS_VCARD}, + IQr = Module:Function(JID, JID, IQ), + [A1] = IQr#iq.sub_el, + case A1#xmlel.children of + [_|_] -> + case get_vcard(Data, A1) of + [false] -> throw(error_no_value_found_in_vcard); + ElemList -> ?DEBUG("ELS ~p", [ElemList]), [xml:get_tag_cdata(Elem) || Elem <- ElemList] + end; + [] -> + throw(error_no_vcard_found) + end. + +get_vcard([<<"TEL">>, TelType], {_, _, _, OldEls}) -> + {TakenEl, _NewEls} = take_vcard_tel(TelType, OldEls, [], not_found), + [TakenEl]; + +get_vcard([Data1, Data2], A1) -> + case get_subtag(A1, Data1) of + [false] -> [false]; + A2List -> + lists:flatten([get_vcard([Data2], A2) || A2 <- A2List]) + end; + +get_vcard([Data], A1) -> + get_subtag(A1, Data). + +get_subtag(Xmlelement, Name) -> + [xml:get_subtag(Xmlelement, Name)]. + +set_vcard_content(User, Server, Data, SomeContent) -> + ContentList = case SomeContent of + [Bin | _] when is_binary(Bin) -> SomeContent; + Bin when is_binary(Bin) -> [SomeContent] + end, + [{_, Module, Function, _Opts}] = ets:lookup(sm_iqtable, {?NS_VCARD, Server}), + JID = jlib:make_jid(User, Server, get_module_resource(Server)), + IQ = #iq{type = get, xmlns = ?NS_VCARD}, + IQr = Module:Function(JID, JID, IQ), + + %% Get old vcard + A4 = case IQr#iq.sub_el of + [A1] -> + {_, _, _, A2} = A1, + update_vcard_els(Data, ContentList, A2); + [] -> + update_vcard_els(Data, ContentList, []) + end, + + %% Build new vcard + SubEl = {xmlel, <<"vCard">>, [{<<"xmlns">>,<<"vcard-temp">>}], A4}, + IQ2 = #iq{type=set, sub_el = SubEl}, + + Module:Function(JID, JID, IQ2), + ok. + +take_vcard_tel(TelType, [{xmlel, <<"TEL">>, _, SubEls}=OldEl | OldEls], NewEls, Taken) -> + {Taken2, NewEls2} = case lists:keymember(TelType, 2, SubEls) of + true -> {xml:get_subtag(OldEl, <<"NUMBER">>), NewEls}; + false -> {Taken, [OldEl | NewEls]} + end, + take_vcard_tel(TelType, OldEls, NewEls2, Taken2); +take_vcard_tel(TelType, [OldEl | OldEls], NewEls, Taken) -> + take_vcard_tel(TelType, OldEls, [OldEl | NewEls], Taken); +take_vcard_tel(_TelType, [], NewEls, Taken) -> + {Taken, NewEls}. + +update_vcard_els([<<"TEL">>, TelType], [TelValue], OldEls) -> + {_, NewEls} = take_vcard_tel(TelType, OldEls, [], not_found), + NewEl = {xmlel,<<"TEL">>,[], + [{xmlel,TelType,[],[]}, + {xmlel,<<"NUMBER">>,[],[{xmlcdata,TelValue}]}]}, + [NewEl | NewEls]; + +update_vcard_els(Data, ContentList, Els1) -> + Els2 = lists:keysort(2, Els1), + [Data1 | Data2] = Data, + NewEls = case Data2 of + [] -> + [{xmlel, Data1, [], [{xmlcdata,Content}]} || Content <- ContentList]; + [D2] -> + OldEl = case lists:keysearch(Data1, 2, Els2) of + {value, A} -> A; + false -> {xmlel, Data1, [], []} + end, + {xmlel, _, _, ContentOld1} = OldEl, + Content2 = [{xmlel, D2, [], [{xmlcdata,Content}]} || Content <- ContentList], + ContentOld2 = [A || {_, X, _, _} = A <- ContentOld1, X/=D2], + ContentOld3 = lists:keysort(2, ContentOld2), + ContentNew = lists:keymerge(2, Content2, ContentOld3), + [{xmlel, Data1, [], ContentNew}] + end, + Els3 = lists:keydelete(Data1, 2, Els2), + lists:keymerge(2, NewEls, Els3). + + +%%% +%%% Roster +%%% + +add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs) -> + case add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs, []) of + {atomic, ok} -> + push_roster_item(LocalUser, LocalServer, User, Server, {add, Nick, Subs, Group}), + ok; + _ -> + error + end. + +add_rosteritem(LU, LS, User, Server, Nick, Group, Subscription, Xattrs) -> + subscribe(LU, LS, User, Server, Nick, Group, Subscription, Xattrs). + +subscribe(LU, LS, User, Server, Nick, Group, Subscription, _Xattrs) -> + ItemEl = build_roster_item(User, Server, {add, Nick, Subscription, Group}), + mod_roster:set_items( + LU, LS, + {xmlel, <<"query">>, + [{<<"xmlns">>, <<"jabber:iq:roster">>}], + [ItemEl]}). + +delete_rosteritem(LocalUser, LocalServer, User, Server) -> + case unsubscribe(LocalUser, LocalServer, User, Server) of + {atomic, ok} -> + push_roster_item(LocalUser, LocalServer, User, Server, remove), + ok; + _ -> + error + end. + +unsubscribe(LU, LS, User, Server) -> + ItemEl = build_roster_item(User, Server, remove), + mod_roster:set_items( + LU, LS, + {xmlel, <<"query">>, + [{<<"xmlns">>, <<"jabber:iq:roster">>}], + [ItemEl]}). + +%% ----------------------------- +%% Get Roster +%% ----------------------------- + +get_roster(User, Server) -> + Items = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]), + make_roster_xmlrpc(Items). + +%% Note: if a contact is in several groups, the contact is returned +%% several times, each one in a different group. +make_roster_xmlrpc(Roster) -> + lists:foldl( + fun(Item, Res) -> + JIDS = jlib:jid_to_string(Item#roster.jid), + Nick = Item#roster.name, + Subs = atom_to_list(Item#roster.subscription), + Ask = atom_to_list(Item#roster.ask), + Groups = case Item#roster.groups of + [] -> [<<>>]; + Gs -> Gs + end, + ItemsX = [{JIDS, Nick, Subs, Ask, Group} || Group <- Groups], + ItemsX ++ Res + end, + [], + Roster). + + +%%----------------------------- +%% Push Roster from file +%%----------------------------- + +push_roster(File, User, Server) -> + {ok, [Roster]} = file:consult(File), + subscribe_roster({User, Server, <<>>, User}, Roster). + +push_roster_all(File) -> + {ok, [Roster]} = file:consult(File), + subscribe_all(Roster). + +subscribe_all(Roster) -> + subscribe_all(Roster, Roster). +subscribe_all([], _) -> + ok; +subscribe_all([User1 | Users], Roster) -> + subscribe_roster(User1, Roster), + subscribe_all(Users, Roster). + +subscribe_roster(_, []) -> + ok; +%% Do not subscribe a user to itself +subscribe_roster({Name, Server, Group, Nick}, [{Name, Server, _, _} | Roster]) -> + subscribe_roster({Name, Server, Group, Nick}, Roster); +%% Subscribe Name2 to Name1 +subscribe_roster({Name1, Server1, Group1, Nick1}, [{Name2, Server2, Group2, Nick2} | Roster]) -> + subscribe(Name1, Server1, Name2, Server2, Nick2, Group2, <<"both">>, []), + subscribe_roster({Name1, Server1, Group1, Nick1}, Roster). + +push_alltoall(S, G) -> + Users = ejabberd_auth:get_vh_registered_users(S), + Users2 = build_list_users(G, Users, []), + subscribe_all(Users2), + ok. + +build_list_users(_Group, [], Res) -> + Res; +build_list_users(Group, [{User, Server}|Users], Res) -> + build_list_users(Group, Users, [{User, Server, Group, User}|Res]). + +%% @spec(LU, LS, U, S, Action) -> ok +%% Action = {add, Nick, Subs, Group} | remove +%% @doc Push to the roster of account LU@LS the contact U@S. +%% The specific action to perform is defined in Action. +push_roster_item(LU, LS, U, S, Action) -> + lists:foreach(fun(R) -> + push_roster_item(LU, LS, R, U, S, Action) + end, ejabberd_sm:get_user_resources(LU, LS)). + +push_roster_item(LU, LS, R, U, S, Action) -> + LJID = jlib:make_jid(LU, LS, R), + BroadcastEl = build_broadcast(U, S, Action), + ejabberd_sm:route(LJID, LJID, BroadcastEl), + Item = build_roster_item(U, S, Action), + ResIQ = build_iq_roster_push(Item), + ejabberd_router:route(LJID, LJID, ResIQ). + +build_roster_item(U, S, {add, Nick, Subs, Group}) -> + {xmlel, <<"item">>, + [{<<"jid">>, jlib:jid_to_string(jlib:make_jid(U, S, <<>>))}, + {<<"name">>, Nick}, + {<<"subscription">>, Subs}], + [{xmlel, <<"group">>, [], [{xmlcdata, Group}]}] + }; +build_roster_item(U, S, remove) -> + {xmlel, <<"item">>, + [{<<"jid">>, jlib:jid_to_string(jlib:make_jid(U, S, <<>>))}, + {<<"subscription">>, <<"remove">>}], + [] + }. + +build_iq_roster_push(Item) -> + {xmlel, <<"iq">>, + [{<<"type">>, <<"set">>}, {<<"id">>, <<"push">>}], + [{xmlel, <<"query">>, + [{<<"xmlns">>, ?NS_ROSTER}], + [Item] + } + ] + }. + +build_broadcast(U, S, {add, _Nick, Subs, _Group}) -> + build_broadcast(U, S, list_to_atom(binary_to_list(Subs))); +build_broadcast(U, S, remove) -> + build_broadcast(U, S, none); +%% @spec (U::binary(), S::binary(), Subs::atom()) -> any() +%% Subs = both | from | to | none +build_broadcast(U, S, SubsAtom) when is_atom(SubsAtom) -> + {broadcast, {item, {U, S, <<>>}, SubsAtom}}. + +%%% +%%% Last Activity +%%% + +get_last(User, Server) -> + Mod = get_lastactivity_module(Server), + case ejabberd_sm:get_user_resources(User, Server) of + [] -> + case Mod:get_last_info(User, Server) of + not_found -> + "Never"; + {ok, Shift, _Status} -> + TimeStamp = {Shift div 1000000, + Shift rem 1000000, + 0}, + {{Year, Month, Day}, {Hour, Minute, Second}} = + calendar:now_to_local_time(TimeStamp), + lists:flatten( + io_lib:format( + "~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", + [Year, Month, Day, Hour, Minute, Second])) + end; + _ -> + "Online" + end. + +set_last(User, Server, Timestamp, Status) -> + Mod = get_lastactivity_module(Server), + Mod:store_last_info(User, Server, Timestamp, Status). + +%%% +%%% Private Storage +%%% + +%% Example usage: +%% $ ejabberdctl private_set badlop localhost "\<aa\ xmlns=\'bb\'\>Cluth\</aa\>" +%% $ ejabberdctl private_get badlop localhost aa bb +%% <aa xmlns='bb'>Cluth</aa> + +private_get(Username, Host, Element, Ns) -> + From = jlib:make_jid(Username, Host, <<>>), + To = jlib:make_jid(Username, Host, <<>>), + IQ = {iq, <<>>, get, ?NS_PRIVATE, <<>>, + {xmlel, <<"query">>, + [{<<"xmlns">>,?NS_PRIVATE}], + [{xmlel, Element, [{<<"xmlns">>, Ns}], []}]}}, + ResIq = mod_private:process_sm_iq(From, To, IQ), + [{xmlel, <<"query">>, + [{<<"xmlns">>, <<"jabber:iq:private">>}], + [SubEl]}] = ResIq#iq.sub_el, + binary_to_list(xml:element_to_binary(SubEl)). + +private_set(Username, Host, ElementString) -> + case xml_stream:parse_element(ElementString) of + {error, Error} -> + io:format("Error found parsing the element:~n ~p~nError: ~p~n", + [ElementString, Error]), + error; + Xml -> + private_set2(Username, Host, Xml) + end. + +private_set2(Username, Host, Xml) -> + From = jlib:make_jid(Username, Host, <<>>), + To = jlib:make_jid(Username, Host, <<>>), + IQ = {iq, <<>>, set, ?NS_PRIVATE, <<>>, + {xmlel, <<"query">>, + [{<<"xmlns">>, ?NS_PRIVATE}], + [Xml]}}, + mod_private:process_sm_iq(From, To, IQ), + ok. + +%%% +%%% Shared Roster Groups +%%% + +srg_create(Group, Host, Name, Description, Display) -> + DisplayList = case Display of + <<>> -> []; + _ -> ejabberd_regexp:split(Display, <<"\\\\n">>) + end, + Opts = [{name, Name}, + {displayed_groups, DisplayList}, + {description, Description}], + {atomic, ok} = mod_shared_roster:create_group(Host, Group, Opts), + ok. + +srg_delete(Group, Host) -> + {atomic, ok} = mod_shared_roster:delete_group(Host, Group), + ok. + +srg_list(Host) -> + lists:sort(mod_shared_roster:list_groups(Host)). + +srg_get_info(Group, Host) -> + Opts = case mod_shared_roster:get_group_opts(Host,Group) of + Os when is_list(Os) -> Os; + error -> [] + end, + [{jlib:atom_to_binary(Title), + io_lib:format("~p", [btl(Value)])} || {Title, Value} <- Opts]. + +btl([]) -> []; +btl([B|L]) -> [btl(B)|btl(L)]; +btl(B) -> binary_to_list(B). + +srg_get_members(Group, Host) -> + Members = mod_shared_roster:get_group_explicit_users(Host,Group), + [jlib:jid_to_string(jlib:make_jid(MUser, MServer, <<>>)) + || {MUser, MServer} <- Members]. + +srg_user_add(User, Host, Group, GroupHost) -> + {atomic, ok} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group), + ok. + +srg_user_del(User, Host, Group, GroupHost) -> + {atomic, ok} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group), + ok. + + +%%% +%%% Stanza +%%% + +%% @doc Send a message to a Jabber account. +%% @spec (Type::binary(), From::binary(), To::binary(), Subject::binary(), Body::binary()) -> ok +send_message(Type, From, To, Subject, Body) -> + Packet = build_packet(Type, Subject, Body), + send_packet_all_resources(From, To, Packet). + +%% @doc Send a packet to a Jabber account. +%% If a resource was specified in the JID, +%% the packet is sent only to that specific resource. +%% If no resource was specified in the JID, +%% and the user is remote or local but offline, +%% the packet is sent to the bare JID. +%% If the user is local and is online in several resources, +%% the packet is sent to all its resources. +send_packet_all_resources(FromJIDString, ToJIDString, Packet) -> + FromJID = jlib:string_to_jid(FromJIDString), + ToJID = jlib:string_to_jid(ToJIDString), + ToUser = ToJID#jid.user, + ToServer = ToJID#jid.server, + case ToJID#jid.resource of + <<>> -> + send_packet_all_resources(FromJID, ToUser, ToServer, Packet); + Res -> + send_packet_all_resources(FromJID, ToUser, ToServer, Res, Packet) + end. + +send_packet_all_resources(FromJID, ToUser, ToServer, Packet) -> + case ejabberd_sm:get_user_resources(ToUser, ToServer) of + [] -> + send_packet_all_resources(FromJID, ToUser, ToServer, <<>>, Packet); + ToResources -> + lists:foreach( + fun(ToResource) -> + send_packet_all_resources(FromJID, ToUser, ToServer, + ToResource, Packet) + end, + ToResources) + end. + +send_packet_all_resources(FromJID, ToU, ToS, ToR, Packet) -> + ToJID = jlib:make_jid(ToU, ToS, ToR), + ejabberd_router:route(FromJID, ToJID, Packet). + +build_packet(Type, Subject, Body) -> + Tail = if Subject == <<"">>; Type == <<"chat">> -> []; + true -> [{xmlel, <<"subject">>, [], [{xmlcdata, Subject}]}] + end, + {xmlel, <<"message">>, + [{<<"type">>, Type}, {<<"id">>, randoms:get_string()}], + [{xmlel, <<"body">>, [], [{xmlcdata, Body}]} | Tail] + }. + +send_stanza_c2s(Username, Host, Resource, Stanza) -> + C2sPid = ejabberd_sm:get_session_pid(Username, Host, Resource), + XmlEl = xml_stream:parse_element(Stanza), + p1_fsm:send_event(C2sPid, {xmlstreamelement, XmlEl}). + +privacy_set(Username, Host, QueryS) -> + From = jlib:make_jid(Username, Host, <<"">>), + To = jlib:make_jid(<<"">>, Host, <<"">>), + QueryEl = xml_stream:parse_element(QueryS), + StanzaEl = {xmlel, <<"iq">>, [{<<"type">>, <<"set">>}], [QueryEl]}, + IQ = jlib:iq_query_info(StanzaEl), + ejabberd_hooks:run_fold( + privacy_iq_set, + Host, + {error, ?ERR_FEATURE_NOT_IMPLEMENTED}, + [From, To, IQ] + ), + ok. + +%%% +%%% Stats +%%% + +stats(Name) -> + case Name of + <<"uptimeseconds">> -> trunc(element(1, erlang:statistics(wall_clock))/1000); + <<"registeredusers">> -> lists:foldl(fun(Host, Sum) -> ejabberd_auth:get_vh_registered_users_number(Host) + Sum end, 0, ?MYHOSTS); + <<"onlineusersnode">> -> length(ejabberd_sm:dirty_get_my_sessions_list()); + <<"onlineusers">> -> length(ejabberd_sm:dirty_get_sessions_list()) + end. + +stats(Name, Host) -> + case Name of + <<"registeredusers">> -> ejabberd_auth:get_vh_registered_users_number(Host); + <<"onlineusers">> -> length(ejabberd_sm:get_vh_session_list(Host)) + end. + + + +%%----------------------------- +%% Purge roster items +%%----------------------------- + +process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> + Action = case ActionS of + "list" -> list; + "delete" -> delete + end, + + Subs = lists:foldl( + fun(any, _) -> [none, from, to, both]; + (Sub, Subs) -> [Sub | Subs] + end, + [], + [list_to_atom(S) || S <- string:tokens(SubsS, ":")] + ), + + Asks = lists:foldl( + fun(any, _) -> [none, out, in]; + (Ask, Asks) -> [Ask | Asks] + end, + [], + [list_to_atom(S) || S <- string:tokens(AsksS, ":")] + ), + + Users = lists:foldl( + fun("any", _) -> ["*", "*@*"]; + (U, Us) -> [U | Us] + end, + [], + [S || S <- string:tokens(UsersS, ":")] + ), + + Contacts = lists:foldl( + fun("any", _) -> ["*", "*@*"]; + (U, Us) -> [U | Us] + end, + [], + [S || S <- string:tokens(ContactsS, ":")] + ), + + rosteritem_purge({Action, Subs, Asks, Users, Contacts}). + +%% @spec ({Action::atom(), Subs::[atom()], Asks::[atom()], User::string(), Contact::string()}) -> {atomic, ok} +rosteritem_purge(Options) -> + Num_rosteritems = mnesia:table_info(roster, size), + io:format("There are ~p roster items in total.~n", [Num_rosteritems]), + Key = mnesia:dirty_first(roster), + rip(Key, Options, {0, Num_rosteritems, 0, 0}, []). + +rip('$end_of_table', _Options, Counters, Res) -> + print_progress_line(Counters), + Res; +rip(Key, Options, {Pr, NT, NV, ND}, Res) -> + Key_next = mnesia:dirty_next(roster, Key), + {Action, _, _, _, _} = Options, + {ND2, Res2} = case decide_rip(Key, Options) of + true -> + Jids = apply_action(Action, Key), + {ND+1, [Jids | Res]}; + false -> + {ND, Res} + end, + NV2 = NV+1, + Pr2 = print_progress_line({Pr, NT, NV2, ND2}), + rip(Key_next, Options, {Pr2, NT, NV2, ND2}, Res2). + +apply_action(list, Key) -> + {User, Server, JID} = Key, + {RUser, RServer, _} = JID, + Jid1string = <<User/binary, "@", Server/binary>>, + Jid2string = <<RUser/binary, "@", RServer/binary>>, + io:format("Matches: ~s ~s~n", [Jid1string, Jid2string]), + {Jid1string, Jid2string}; +apply_action(delete, Key) -> + R = apply_action(list, Key), + mnesia:dirty_delete(roster, Key), + R. + +print_progress_line({_Pr, 0, _NV, _ND}) -> + ok; +print_progress_line({Pr, NT, NV, ND}) -> + Pr2 = trunc((NV/NT)*100), + case Pr == Pr2 of + true -> + ok; + false -> + io:format("Progress ~p% - visited ~p - deleted ~p~n", [Pr2, NV, ND]) + end, + Pr2. + +decide_rip(Key, {_Action, Subs, Asks, User, Contact}) -> + case catch mnesia:dirty_read(roster, Key) of + [RI] -> + lists:member(RI#roster.subscription, Subs) + andalso lists:member(RI#roster.ask, Asks) + andalso decide_rip_jid(RI#roster.us, User) + andalso decide_rip_jid(RI#roster.jid, Contact); + _ -> + false + end. + +%% Returns true if the server of the JID is included in the servers +decide_rip_jid({UName, UServer, _UResource}, Match_list) -> + decide_rip_jid({UName, UServer}, Match_list); +decide_rip_jid({UName, UServer}, Match_list) -> + lists:any( + fun(Match_string) -> + MJID = jlib:string_to_jid(list_to_binary(Match_string)), + MName = MJID#jid.luser, + MServer = MJID#jid.lserver, + Is_server = is_glob_match(UServer, MServer), + case MName of + <<>> when UName == <<>> -> + Is_server; + <<>> -> + false; + _ -> + Is_server + andalso is_glob_match(UName, MName) + end + end, + Match_list). + +%% Copied from ejabberd-2.0.0/src/acl.erl +is_regexp_match(String, RegExp) -> + case ejabberd_regexp:run(String, RegExp) of + nomatch -> + false; + match -> + true; + {error, ErrDesc} -> + io:format( + "Wrong regexp ~p in ACL: ~p", + [RegExp, ErrDesc]), + false + end. +is_glob_match(String, <<"!", Glob/binary>>) -> + not is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)); +is_glob_match(String, Glob) -> + is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)). diff --git a/src/mod_announce.erl b/src/mod_announce.erl index 0e7c9fa32..542417ff3 100644 --- a/src/mod_announce.erl +++ b/src/mod_announce.erl @@ -64,7 +64,7 @@ tokenize(Node) -> str:tokens(Node, <<"/#">>). start(Host, Opts) -> - case gen_mod:db_type(Opts) of + case gen_mod:db_type(Host, Opts) of mnesia -> mnesia:create_table(motd, [{disc_copies, [node()]}, diff --git a/src/mod_blocking.erl b/src/mod_blocking.erl index 07e9027b6..172786810 100644 --- a/src/mod_blocking.erl +++ b/src/mod_blocking.erl @@ -17,9 +17,10 @@ %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% -%%% You should have received a copy of the GNU General Public License along -%%% with this program; if not, write to the Free Software Foundation, Inc., -%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% You should have received a copy of the GNU General Public License +%%% along with this program; if not, write to the Free Software +%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA %%% %%%---------------------------------------------------------------------- @@ -151,7 +152,9 @@ process_blocklist_block(LUser, LServer, JIDs) -> broadcast_blocklist_event(LUser, LServer, {block, JIDs}), {result, [], UserList}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} + _Err -> + ?ERROR_MSG("Error processing ~p: ~p", [{LUser, LServer, JIDs}, _Err]), + {error, ?ERR_INTERNAL_SERVER_ERROR} end. process_blocklist_block(LUser, LServer, Filter, @@ -236,7 +239,7 @@ process_blocklist_block(LUser, LServer, Filter, odbc) -> <<"match_all">>, <<"match_iq">>, <<"match_message">>, <<"match_presence_in">>, <<"match_presence_out">>], RItems = [_ | _]} -> - List = lists:map(fun mod_privacy:raw_to_item/1, RItems); + List = lists:flatmap(fun mod_privacy:raw_to_item/1, RItems); _ -> List = [] end, NewList = Filter(List), @@ -253,10 +256,8 @@ process_blocklist_unblock_all(LUser, LServer) -> end, List) end, - case process_blocklist_unblock_all(LUser, LServer, - Filter, - gen_mod:db_type(LServer, mod_privacy)) - of + DBType = gen_mod:db_type(LServer, mod_privacy), + case unblock_by_filter(LUser, LServer, Filter, DBType) of {atomic, ok} -> {result, []}; {atomic, {ok, Default, List}} -> UserList = make_userlist(Default, List), @@ -264,86 +265,11 @@ process_blocklist_unblock_all(LUser, LServer) -> UserList), broadcast_blocklist_event(LUser, LServer, unblock_all), {result, [], UserList}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} + _Err -> + ?ERROR_MSG("Error processing ~p: ~p", [{LUser, LServer}, _Err]), + {error, ?ERR_INTERNAL_SERVER_ERROR} end. -process_blocklist_unblock_all(LUser, LServer, Filter, - mnesia) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> - % No lists, nothing to unblock - ok; - [#privacy{default = Default, lists = Lists} = P] -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewList = Filter(List), - NewLists1 = lists:keydelete(Default, 1, Lists), - NewLists = [{Default, NewList} | NewLists1], - mnesia:write(P#privacy{lists = NewLists}), - {ok, Default, NewList}; - false -> - % No default list, nothing to unblock - ok - end - end - end, - mnesia:transaction(F); -process_blocklist_unblock_all(LUser, LServer, Filter, - riak) -> - {atomic, - case ejabberd_riak:get(privacy, {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists} = P} -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewList = Filter(List), - NewLists1 = lists:keydelete(Default, 1, Lists), - NewLists = [{Default, NewList} | NewLists1], - case ejabberd_riak:put(P#privacy{lists = NewLists}, - mod_privacy:privacy_schema()) of - ok -> - {ok, Default, NewList}; - Err -> - Err - end; - false -> - %% No default list, nothing to unblock - ok - end; - {error, _} -> - %% No lists, nothing to unblock - ok - end}; -process_blocklist_unblock_all(LUser, LServer, Filter, - odbc) -> - F = fun () -> - case mod_privacy:sql_get_default_privacy_list_t(LUser) - of - {selected, [<<"name">>], []} -> ok; - {selected, [<<"name">>], [[Default]]} -> - {selected, [<<"id">>], [[ID]]} = - mod_privacy:sql_get_privacy_list_id_t(LUser, Default), - case mod_privacy:sql_get_privacy_list_data_by_id_t(ID) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems = [_ | _]} -> - List = lists:map(fun mod_privacy:raw_to_item/1, - RItems), - NewList = Filter(List), - NewRItems = lists:map(fun mod_privacy:item_to_raw/1, - NewList), - mod_privacy:sql_set_privacy_list(ID, NewRItems), - {ok, Default, NewList}; - _ -> ok - end; - _ -> ok - end - end, - ejabberd_odbc:sql_transaction(LServer, F). - process_blocklist_unblock(LUser, LServer, JIDs) -> Filter = fun (List) -> lists:filter(fun (#listitem{action = deny, type = jid, @@ -353,9 +279,8 @@ process_blocklist_unblock(LUser, LServer, JIDs) -> end, List) end, - case process_blocklist_unblock(LUser, LServer, Filter, - gen_mod:db_type(LServer, mod_privacy)) - of + DBType = gen_mod:db_type(LServer, mod_privacy), + case unblock_by_filter(LUser, LServer, Filter, DBType) of {atomic, ok} -> {result, []}; {atomic, {ok, Default, List}} -> UserList = make_userlist(Default, List), @@ -364,11 +289,12 @@ process_blocklist_unblock(LUser, LServer, JIDs) -> broadcast_blocklist_event(LUser, LServer, {unblock, JIDs}), {result, [], UserList}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} + _Err -> + ?ERROR_MSG("Error processing ~p: ~p", [{LUser, LServer, JIDs}, _Err]), + {error, ?ERR_INTERNAL_SERVER_ERROR} end. -process_blocklist_unblock(LUser, LServer, Filter, - mnesia) -> +unblock_by_filter(LUser, LServer, Filter, mnesia) -> F = fun () -> case mnesia:read({privacy, {LUser, LServer}}) of [] -> @@ -389,8 +315,7 @@ process_blocklist_unblock(LUser, LServer, Filter, end end, mnesia:transaction(F); -process_blocklist_unblock(LUser, LServer, Filter, - riak) -> +unblock_by_filter(LUser, LServer, Filter, riak) -> {atomic, case ejabberd_riak:get(privacy, mod_privacy:privacy_schema(), {LUser, LServer}) of @@ -415,8 +340,7 @@ process_blocklist_unblock(LUser, LServer, Filter, ok end end}; -process_blocklist_unblock(LUser, LServer, Filter, - odbc) -> +unblock_by_filter(LUser, LServer, Filter, odbc) -> F = fun () -> case mod_privacy:sql_get_default_privacy_list_t(LUser) of @@ -431,8 +355,8 @@ process_blocklist_unblock(LUser, LServer, Filter, <<"match_all">>, <<"match_iq">>, <<"match_message">>, <<"match_presence_in">>, <<"match_presence_out">>], RItems = [_ | _]} -> - List = lists:map(fun mod_privacy:raw_to_item/1, - RItems), + List = lists:flatmap(fun mod_privacy:raw_to_item/1, + RItems), NewList = Filter(List), NewRItems = lists:map(fun mod_privacy:item_to_raw/1, NewList), @@ -520,7 +444,7 @@ process_blocklist_get(LUser, LServer, odbc) -> <<"match_all">>, <<"match_iq">>, <<"match_message">>, <<"match_presence_in">>, <<"match_presence_out">>], RItems} -> - lists:map(fun mod_privacy:raw_to_item/1, RItems); + lists:flatmap(fun mod_privacy:raw_to_item/1, RItems); {'EXIT', _} -> error end; {'EXIT', _} -> error diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 5c6d041f8..36c8c0eed 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -17,10 +17,9 @@ %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% -%%% You should have received a copy of the GNU General Public License -%%% along with this program; if not, write to the Free Software -%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA -%%% 02111-1307 USA +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%% 2009, improvements from ProcessOne to support correct PEP handling %%% through s2s, use less memory, and speedup global caps handling @@ -322,7 +321,7 @@ init_db(_, _) -> ok. init([Host, Opts]) -> - init_db(gen_mod:db_type(Opts), Host), + init_db(gen_mod:db_type(Host, Opts), Host), MaxSize = gen_mod:get_opt(cache_size, Opts, fun(I) when is_integer(I), I>0 -> I end, 1000), diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl index 7464cdb69..24c09bffd 100644 --- a/src/mod_carboncopy.erl +++ b/src/mod_carboncopy.erl @@ -153,9 +153,7 @@ check_and_forward(JID, To, Packet, Direction)-> end; _ -> ok - end; - -check_and_forward(_JID, _To, _Packet, _)-> ok. + end. remove_connection(User, Server, Resource, _Status)-> disable(Server, User, Resource), @@ -167,19 +165,16 @@ remove_connection(User, Server, Resource, _Status)-> send_copies(JID, To, Packet, Direction)-> {U, S, R} = jlib:jid_tolower(JID), PrioRes = ejabberd_sm:get_user_present_resources(U, S), + {_, AvailRs} = lists:unzip(PrioRes), {MaxPrio, MaxRes} = case catch lists:max(PrioRes) of {Prio, Res} -> {Prio, Res}; _ -> {0, undefined} end, + %% unavailable resources are handled like bare JIDs IsBareTo = case {Direction, To} of {received, #jid{lresource = <<>>}} -> true; - {received, #jid{lresource = LRes}} -> - %% unavailable resources are handled like bare JIDs - case lists:keyfind(LRes, 2, PrioRes) of - false -> true; - _ -> false - end; + {received, #jid{lresource = LRes}} -> not lists:member(LRes, AvailRs); _ -> false end, %% list of JIDs that should receive a carbon copy of this message (excluding the @@ -188,7 +183,8 @@ send_copies(JID, To, Packet, Direction)-> {true, MaxRes} -> OrigTo = fun(Res) -> lists:member({MaxPrio, Res}, PrioRes) end, [ {jlib:make_jid({U, S, CCRes}), CC_Version} - || {CCRes, CC_Version} <- list(U, S), not OrigTo(CCRes) ]; + || {CCRes, CC_Version} <- list(U, S), + lists:member(CCRes, AvailRs), not OrigTo(CCRes) ]; {true, _} -> %% The message was sent to our bare JID, and we currently have %% multiple resources with the same highest priority, so the session @@ -198,7 +194,8 @@ send_copies(JID, To, Packet, Direction)-> []; {false, _} -> [ {jlib:make_jid({U, S, CCRes}), CC_Version} - || {CCRes, CC_Version} <- list(U, S), CCRes /= R ] + || {CCRes, CC_Version} <- list(U, S), + lists:member(CCRes, AvailRs), CCRes /= R ] %TargetJIDs = lists:delete(JID, [ jlib:make_jid({U, S, CCRes}) || CCRes <- list(U, S) ]), end, diff --git a/src/mod_irc.erl b/src/mod_irc.erl index 2cc57786c..e96fc4dc1 100644 --- a/src/mod_irc.erl +++ b/src/mod_irc.erl @@ -115,7 +115,7 @@ init([Host, Opts]) -> ejabberd:start_app(p1_iconv), MyHost = gen_mod:get_opt_host(Host, Opts, <<"irc.@HOST@">>), - case gen_mod:db_type(Opts) of + case gen_mod:db_type(Host, Opts) of mnesia -> mnesia:create_table(irc_custom, [{disc_copies, [node()]}, diff --git a/src/mod_irc_connection.erl b/src/mod_irc_connection.erl index c31adf754..cc21b0f14 100644 --- a/src/mod_irc_connection.erl +++ b/src/mod_irc_connection.erl @@ -51,12 +51,12 @@ encoding = <<"">> :: binary(), port = 0 :: inet:port_number(), password = <<"">> :: binary(), - queue = queue:new() :: queue(), + queue = queue:new() :: ?TQUEUE, user = #jid{} :: jid(), host = <<"">> :: binary(), server = <<"">> :: binary(), nick = <<"">> :: binary(), - channels = dict:new() :: dict(), + channels = dict:new() :: ?TDICT, nickchannel :: binary(), mod = mod_irc :: atom(), inbuf = <<"">> :: binary(), @@ -697,9 +697,9 @@ terminate(_Reason, _StateName, FullStateData) -> <<"Server Connect Failed">>}]}, FullStateData} end, - (FullStateData#state.mod):closed_connection(StateData#state.host, - StateData#state.user, - StateData#state.server), + (StateData#state.mod):closed_connection(StateData#state.host, + StateData#state.user, + StateData#state.server), bounce_messages(<<"Server Connect Failed">>), lists:foreach(fun (Chan) -> Stanza = #xmlel{name = <<"presence">>, diff --git a/src/mod_last.erl b/src/mod_last.erl index 038378c7b..e079a2d38 100644 --- a/src/mod_last.erl +++ b/src/mod_last.erl @@ -48,7 +48,7 @@ start(Host, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, one_queue), - case gen_mod:db_type(Opts) of + case gen_mod:db_type(Host, Opts) of mnesia -> mnesia:create_table(last_activity, [{disc_copies, [node()]}, diff --git a/src/mod_muc.erl b/src/mod_muc.erl index a0c6c34e6..a3a8a9331 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -283,7 +283,7 @@ can_use_nick(LServer, Host, JID, Nick, odbc) -> init([Host, Opts]) -> MyHost = gen_mod:get_opt_host(Host, Opts, <<"conference.@HOST@">>), - case gen_mod:db_type(Opts) of + case gen_mod:db_type(Host, Opts) of mnesia -> mnesia:create_table(muc_room, [{disc_copies, [node()]}, diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl new file mode 100644 index 000000000..9c69628be --- /dev/null +++ b/src/mod_muc_admin.erl @@ -0,0 +1,888 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_muc_admin.erl +%%% Author : Badlop <badlop@ono.com> +%%% Purpose : Tools for additional MUC administration +%%% Created : 8 Sep 2007 by Badlop <badlop@ono.com> +%%% Id : $Id: mod_muc_admin.erl 1133 2012-10-17 22:13:06Z badlop $ +%%%---------------------------------------------------------------------- + +-module(mod_muc_admin). +-author('badlop@ono.com'). + +-behaviour(gen_mod). + +-export([ + start/2, stop/1, % gen_mod API + muc_online_rooms/1, + muc_unregister_nick/1, + create_room/3, destroy_room/3, + create_rooms_file/1, destroy_rooms_file/1, + rooms_unused_list/2, rooms_unused_destroy/2, + get_room_occupants/2, + get_room_occupants_number/2, + send_direct_invitation/4, + change_room_option/4, + set_room_affiliation/4, + get_room_affiliations/2, + web_menu_main/2, web_page_main/2, % Web Admin API + web_menu_host/3, web_page_host/3 + ]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). +-include("jlib.hrl"). +-include("mod_muc_room.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). +-include("ejabberd_commands.hrl"). + +%% Copied from mod_muc/mod_muc.erl +-record(muc_online_room, {name_host, pid}). + +%%---------------------------- +%% gen_mod +%%---------------------------- + +start(Host, _Opts) -> + ejabberd_commands:register_commands(commands()), + ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50), + ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), + ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50), + ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, web_page_host, 50). + +stop(Host) -> + ejabberd_commands:unregister_commands(commands()), + ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50), + ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), + ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50), + ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, web_page_host, 50). + +%%% +%%% Register commands +%%% + +commands() -> + [ + #ejabberd_commands{name = muc_online_rooms, tags = [muc], + desc = "List existing rooms ('global' to get all vhosts)", + module = ?MODULE, function = muc_online_rooms, + args = [{host, binary}], + result = {rooms, {list, {room, string}}}}, + #ejabberd_commands{name = muc_unregister_nick, tags = [muc], + desc = "Unregister the nick in the MUC service", + module = ?MODULE, function = muc_unregister_nick, + args = [{nick, binary}], + result = {res, rescode}}, + + #ejabberd_commands{name = create_room, tags = [muc_room], + desc = "Create a MUC room name@service in host", + module = ?MODULE, function = create_room, + args = [{name, binary}, {service, binary}, + {host, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = destroy_room, tags = [muc_room], + desc = "Destroy a MUC room", + module = ?MODULE, function = destroy_room, + args = [{name, binary}, {service, binary}, + {host, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = create_rooms_file, tags = [muc], + desc = "Create the rooms indicated in file", + module = ?MODULE, function = create_rooms_file, + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = destroy_rooms_file, tags = [muc], + desc = "Destroy the rooms indicated in file", + module = ?MODULE, function = destroy_rooms_file, + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = rooms_unused_list, tags = [muc], + desc = "List the rooms that are unused for many days in host", + module = ?MODULE, function = rooms_unused_list, + args = [{host, binary}, {days, integer}], + result = {rooms, {list, {room, string}}}}, + #ejabberd_commands{name = rooms_unused_destroy, tags = [muc], + desc = "Destroy the rooms that are unused for many days in host", + module = ?MODULE, function = rooms_unused_destroy, + args = [{host, binary}, {days, integer}], + result = {rooms, {list, {room, string}}}}, + + #ejabberd_commands{name = get_room_occupants, tags = [muc_room], + desc = "Get the list of occupants of a MUC room", + module = ?MODULE, function = get_room_occupants, + args = [{name, binary}, {service, binary}], + result = {occupants, {list, + {occupant, {tuple, + [{jid, string}, + {nick, string}, + {role, string} + ]}} + }}}, + + #ejabberd_commands{name = get_room_occupants_number, tags = [muc_room], + desc = "Get the number of occupants of a MUC room", + module = ?MODULE, function = get_room_occupants_number, + args = [{name, binary}, {service, binary}], + result = {occupants, integer}}, + + #ejabberd_commands{name = send_direct_invitation, tags = [muc_room], + desc = "Send a direct invitation to several destinations", + longdesc = "Password and Message can also be: none. Users JIDs are separated with : ", + module = ?MODULE, function = send_direct_invitation, + args = [{room, binary}, {password, binary}, {reason, binary}, {users, binary}], + result = {res, rescode}}, + + #ejabberd_commands{name = change_room_option, tags = [muc_room], + desc = "Change an option in a MUC room", + module = ?MODULE, function = change_room_option, + args = [{name, binary}, {service, binary}, + {option, binary}, {value, binary}], + result = {res, rescode}}, + + #ejabberd_commands{name = set_room_affiliation, tags = [muc_room], + desc = "Change an affiliation in a MUC room", + module = ?MODULE, function = set_room_affiliation, + args = [{name, binary}, {service, binary}, + {jid, binary}, {affiliation, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = get_room_affiliations, tags = [muc_room], + desc = "Get the list of affiliations of a MUC room", + module = ?MODULE, function = get_room_affiliations, + args = [{name, binary}, {service, binary}], + result = {affiliations, {list, + {affiliation, {tuple, + [{username, string}, + {domain, string}, + {affiliation, atom}, + {reason, string} + ]}} + }}} + ]. + + +%%% +%%% ejabberd commands +%%% + +muc_online_rooms(ServerHost) -> + MUCHost = find_host(ServerHost), + Rooms = ets:tab2list(muc_online_room), + lists:foldl( + fun({_, {Roomname, Host}, _}, Results) -> + case MUCHost of + global -> + [<<Roomname/binary, "@", Host/binary>> | Results]; + Host -> + [<<Roomname/binary, "@", Host/binary>> | Results]; + _ -> + Results + end + end, + [], + Rooms). + +muc_unregister_nick(Nick) -> + F2 = fun(N) -> + [{_,Key,_}] = mnesia:index_read(muc_registered, N, 3), + mnesia:delete({muc_registered, Key}) + end, + case mnesia:transaction(F2, [Nick], 1) of + {atomic, ok} -> + ok; + {aborted, _Error} -> + error + end. + + +%%---------------------------- +%% Ad-hoc commands +%%---------------------------- + + +%%---------------------------- +%% Web Admin +%%---------------------------- + +%%--------------- +%% Web Admin Menu + +web_menu_main(Acc, Lang) -> + Acc ++ [{<<"muc">>, ?T(<<"Multi-User Chat">>)}]. + +web_menu_host(Acc, _Host, Lang) -> + Acc ++ [{<<"muc">>, ?T(<<"Multi-User Chat">>)}]. + + +%%--------------- +%% Web Admin Page + +-define(TDTD(L, N), + ?XE(<<"tr">>, [?XCT(<<"td">>, L), + ?XC(<<"td">>, jlib:integer_to_binary(N)) + ])). + +web_page_main(_, #request{path=[<<"muc">>], lang = Lang} = _Request) -> + Res = [?XC(<<"h1">>, <<"Multi-User Chat">>), + ?XC(<<"h3">>, <<"Statistics">>), + ?XAE(<<"table">>, [], + [?XE(<<"tbody">>, [?TDTD(<<"Total rooms">>, ets:info(muc_online_room, size)), + ?TDTD(<<"Permanent rooms">>, mnesia:table_info(muc_room, size)), + ?TDTD(<<"Registered nicknames">>, mnesia:table_info(muc_registered, size)) + ]) + ]), + ?XE(<<"ul">>, [?LI([?ACT(<<"rooms">>, <<"List of rooms">>)])]) + ], + {stop, Res}; + +web_page_main(_, #request{path=[<<"muc">>, <<"rooms">>], q = Q, lang = Lang} = _Request) -> + Sort_query = get_sort_query(Q), + Res = make_rooms_page(global, Lang, Sort_query), + {stop, Res}; + +web_page_main(Acc, _) -> Acc. + +web_page_host(_, Host, + #request{path = [<<"muc">>], + q = Q, + lang = Lang} = _Request) -> + Sort_query = get_sort_query(Q), + Res = make_rooms_page(find_host(Host), Lang, Sort_query), + {stop, Res}; +web_page_host(Acc, _, _) -> Acc. + + +%% Returns: {normal | reverse, Integer} +get_sort_query(Q) -> + case catch get_sort_query2(Q) of + {ok, Res} -> Res; + _ -> {normal, 1} + end. + +get_sort_query2(Q) -> + {value, {_, String}} = lists:keysearch(<<"sort">>, 1, Q), + Integer = list_to_integer(binary_to_list(String)), + case Integer >= 0 of + true -> {ok, {normal, Integer}}; + false -> {ok, {reverse, abs(Integer)}} + end. + +make_rooms_page(Host, Lang, {Sort_direction, Sort_column}) -> + Rooms_names = get_rooms(Host), + Rooms_infos = build_info_rooms(Rooms_names), + Rooms_sorted = sort_rooms(Sort_direction, Sort_column, Rooms_infos), + Rooms_prepared = prepare_rooms_infos(Rooms_sorted), + TList = lists:map( + fun(Room) -> + ?XE(<<"tr">>, [?XC(<<"td">>, E) || E <- Room]) + end, Rooms_prepared), + Titles = [<<"Jabber ID">>, + <<"# participants">>, + <<"Last message">>, + <<"Public">>, + <<"Persistent">>, + <<"Logging">>, + <<"Just created">>, + <<"Title">>], + {Titles_TR, _} = + lists:mapfoldl( + fun(Title, Num_column) -> + NCS = jlib:integer_to_binary(Num_column), + TD = ?XE(<<"td">>, [?CT(Title), + ?C(<<" ">>), + ?ACT(<<"?sort=", NCS/binary>>, <<"<">>), + ?C(<<" ">>), + ?ACT(<<"?sort=-", NCS/binary>>, <<">">>)]), + {TD, Num_column+1} + end, + 1, + Titles), + [?XC(<<"h1">>, <<"Multi-User Chat">>), + ?XC(<<"h2">>, <<"Rooms">>), + ?XE(<<"table">>, + [?XE(<<"thead">>, + [?XE(<<"tr">>, Titles_TR)] + ), + ?XE(<<"tbody">>, TList) + ] + ) + ]. + +sort_rooms(Direction, Column, Rooms) -> + Rooms2 = lists:keysort(Column, Rooms), + case Direction of + normal -> Rooms2; + reverse -> lists:reverse(Rooms2) + end. + +build_info_rooms(Rooms) -> + [build_info_room(Room) || Room <- Rooms]. + +build_info_room({Name, Host, Pid}) -> + C = get_room_config(Pid), + Title = C#config.title, + Public = C#config.public, + Persistent = C#config.persistent, + Logging = C#config.logging, + + S = get_room_state(Pid), + Just_created = S#state.just_created, + Num_participants = length(dict:fetch_keys(S#state.users)), + + History = (S#state.history)#lqueue.queue, + Ts_last_message = + case queue:is_empty(History) of + true -> + <<"A long time ago">>; + false -> + Last_message1 = queue:last(History), + {_, _, _, Ts_last, _} = Last_message1, + jlib:timestamp_to_iso(Ts_last) + end, + + {<<Name/binary, "@", Host/binary>>, + Num_participants, + Ts_last_message, + Public, + Persistent, + Logging, + Just_created, + Title}. + +prepare_rooms_infos(Rooms) -> + [prepare_room_info(Room) || Room <- Rooms]. +prepare_room_info(Room_info) -> + {NameHost, + Num_participants, + Ts_last_message, + Public, + Persistent, + Logging, + Just_created, + Title} = Room_info, + [NameHost, + jlib:integer_to_binary(Num_participants), + Ts_last_message, + jlib:atom_to_binary(Public), + jlib:atom_to_binary(Persistent), + jlib:atom_to_binary(Logging), + jlib:atom_to_binary(Just_created), + Title]. + + +%%---------------------------- +%% Create/Delete Room +%%---------------------------- + +%% @spec (Name::binary(), Host::binary(), ServerHost::binary()) -> +%% ok | error +%% @doc Create a room immediately with the default options. +create_room(Name, Host, ServerHost) -> + + %% Get the default room options from the muc configuration + DefRoomOpts = gen_mod:get_module_opt(ServerHost, mod_muc, + default_room_options, fun(X) -> X end, []), + + %% Store the room on the server, it is not started yet though at this point + mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts), + + %% Get all remaining mod_muc parameters that might be utilized + Access = gen_mod:get_module_opt(ServerHost, mod_muc, access, fun(X) -> X end, all), + AcCreate = gen_mod:get_module_opt(ServerHost, mod_muc, access_create, fun(X) -> X end, all), + AcAdmin = gen_mod:get_module_opt(ServerHost, mod_muc, access_admin, fun(X) -> X end, none), + AcPer = gen_mod:get_module_opt(ServerHost, mod_muc, access_persistent, fun(X) -> X end, all), + _PersistHistory = gen_mod:get_module_opt(ServerHost, mod_muc, persist_history, fun(X) -> X end, false), + HistorySize = gen_mod:get_module_opt(ServerHost, mod_muc, history_size, fun(X) -> X end, 20), + RoomShaper = gen_mod:get_module_opt(ServerHost, mod_muc, room_shaper, fun(X) -> X end, none), + + %% If the room does not exist yet in the muc_online_room + case mnesia:dirty_read(muc_online_room, {Name, Host}) of + [] -> + %% Start the room + {ok, Pid} = mod_muc_room:start( + Host, + ServerHost, + {Access, AcCreate, AcAdmin, AcPer}, + Name, + HistorySize, + RoomShaper, + DefRoomOpts), + {atomic, ok} = register_room(Host, Name, Pid), + ok; + _ -> + error + end. + +register_room(Host, Name, Pid) -> + F = fun() -> + mnesia:write(#muc_online_room{name_host = {Name, Host}, + pid = Pid}) + end, + mnesia:transaction(F). + +%% Create the room only in the database. +%% It is required to restart the MUC service for the room to appear. +muc_create_room(ServerHost, {Name, Host, _}, DefRoomOpts) -> + io:format("Creating room ~s@~s~n", [Name, Host]), + mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts). + +%% @spec (Name::binary(), Host::binary(), ServerHost::binary()) -> +%% ok | {error, room_not_exists} +%% @doc Destroy the room immediately. +%% If the room has participants, they are not notified that the room was destroyed; +%% they will notice when they try to chat and receive an error that the room doesn't exist. +destroy_room(Name, Service, _Server) -> + case mnesia:dirty_read(muc_online_room, {Name, Service}) of + [R] -> + Pid = R#muc_online_room.pid, + gen_fsm:send_all_state_event(Pid, destroy), + ok; + [] -> + error + end. + +destroy_room({N, H, SH}) -> + io:format("Destroying room: ~s@~s - vhost: ~s~n", [N, H, SH]), + destroy_room(N, H, SH). + + +%%---------------------------- +%% Destroy Rooms in File +%%---------------------------- + +%% The format of the file is: one chatroom JID per line +%% The file encoding must be UTF-8 + +destroy_rooms_file(Filename) -> + {ok, F} = file:open(Filename, [read]), + RJID = read_room(F), + Rooms = read_rooms(F, RJID, []), + file:close(F), + [destroy_room(A) || A <- Rooms], + ok. + +read_rooms(_F, eof, L) -> + L; + +read_rooms(F, RJID, L) -> + RJID2 = read_room(F), + read_rooms(F, RJID2, [RJID | L]). + +read_room(F) -> + case io:get_line(F, "") of + eof -> eof; + String -> + case io_lib:fread("~s", String) of + {ok, [RoomJID], _} -> split_roomjid(RoomJID); + {error, What} -> + io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String]) + end + end. + +%% This function is quite rudimentary +%% and may not be accurate +split_roomjid(RoomJID) -> + [Name, Host] = string:tokens(RoomJID, "@"), + [_MUC_service_name | ServerHostList] = string:tokens(Host, "."), + ServerHost = join(ServerHostList, "."), + {Name, Host, ServerHost}. + +%% This function is copied from string:join/2 in Erlang/OTP R12B-1 +%% Note that string:join/2 is not implemented in Erlang/OTP R11B +join([H|T], Sep) -> + H ++ lists:concat([Sep ++ X || X <- T]). + + +%%---------------------------- +%% Create Rooms in File +%%---------------------------- + +create_rooms_file(Filename) -> + {ok, F} = file:open(Filename, [read]), + RJID = read_room(F), + Rooms = read_rooms(F, RJID, []), + file:close(F), + %% Read the default room options defined for the first virtual host + DefRoomOpts = gen_mod:get_module_opt(?MYNAME, mod_muc, + default_room_options, + fun(L) when is_list(L) -> L end, []), + [muc_create_room(?MYNAME, A, DefRoomOpts) || A <- Rooms], + ok. + + +%%---------------------------- +%% List/Delete Unused Rooms +%%---------------------------- + +%%--------------- +%% Control + +rooms_unused_list(Host, Days) -> + rooms_unused_report(list, Host, Days). +rooms_unused_destroy(Host, Days) -> + rooms_unused_report(destroy, Host, Days). + +rooms_unused_report(Action, Host, Days) -> + {NA, NP, RP} = muc_unused(Action, Host, Days), + io:format("Unused rooms: ~p out of ~p~n", [NP, NA]), + [[R, <<"@">>, H] || {R, H, _P} <- RP]. + +muc_unused(Action, ServerHost, Days) -> + Host = find_host(ServerHost), + muc_unused2(Action, ServerHost, Host, Days). + +muc_unused2(Action, ServerHost, Host, Last_allowed) -> + %% Get all required info about all existing rooms + Rooms_all = get_rooms(Host), + + %% Decide which ones pass the requirements + Rooms_pass = decide_rooms(Rooms_all, Last_allowed), + + Num_rooms_all = length(Rooms_all), + Num_rooms_pass = length(Rooms_pass), + + %% Perform the desired action for matching rooms + act_on_rooms(Action, Rooms_pass, ServerHost), + + {Num_rooms_all, Num_rooms_pass, Rooms_pass}. + +%%--------------- +%% Get info + +get_rooms(Host) -> + Get_room_names = fun(Room_reg, Names) -> + Pid = Room_reg#muc_online_room.pid, + case {Host, Room_reg#muc_online_room.name_host} of + {Host, {Name1, Host}} -> + [{Name1, Host, Pid} | Names]; + {global, {Name1, Host1}} -> + [{Name1, Host1, Pid} | Names]; + _ -> + Names + end + end, + ets:foldr(Get_room_names, [], muc_online_room). + +get_room_config(Room_pid) -> + {ok, R} = gen_fsm:sync_send_all_state_event(Room_pid, get_config), + R. + +get_room_state(Room_pid) -> + {ok, R} = gen_fsm:sync_send_all_state_event(Room_pid, get_state), + R. + +%%--------------- +%% Decide + +decide_rooms(Rooms, Last_allowed) -> + Decide = fun(R) -> decide_room(R, Last_allowed) end, + lists:filter(Decide, Rooms). + +decide_room({_Room_name, _Host, Room_pid}, Last_allowed) -> + C = get_room_config(Room_pid), + Persistent = C#config.persistent, + + S = get_room_state(Room_pid), + Just_created = S#state.just_created, + + Room_users = S#state.users, + Num_users = length(?DICT:to_list(Room_users)), + + History = (S#state.history)#lqueue.queue, + Ts_now = calendar:now_to_universal_time(now()), + Ts_uptime = uptime_seconds(), + {Has_hist, Last} = case queue:is_empty(History) of + true -> + {false, Ts_uptime}; + false -> + Last_message = queue:last(History), + {_, _, _, Ts_last, _} = Last_message, + Ts_diff = + calendar:datetime_to_gregorian_seconds(Ts_now) + - calendar:datetime_to_gregorian_seconds(Ts_last), + {true, Ts_diff} + end, + + case {Persistent, Just_created, Num_users, Has_hist, seconds_to_days(Last)} of + {_true, false, 0, _, Last_days} + when Last_days >= Last_allowed -> + true; + _ -> + false + end. + +seconds_to_days(S) -> + S div (60*60*24). + +%%--------------- +%% Act + +act_on_rooms(Action, Rooms, ServerHost) -> + ServerHosts = [ {A, find_host(A)} || A <- ?MYHOSTS ], + Delete = fun({_N, H, _Pid} = Room) -> + SH = case ServerHost of + global -> find_serverhost(H, ServerHosts); + O -> O + end, + + act_on_room(Action, Room, SH) + end, + lists:foreach(Delete, Rooms). + +find_serverhost(Host, ServerHosts) -> + {value, {ServerHost, Host}} = lists:keysearch(Host, 2, ServerHosts), + ServerHost. + +act_on_room(destroy, {N, H, Pid}, SH) -> + gen_fsm:send_all_state_event( + Pid, {destroy, <<"Room destroyed by rooms_unused_destroy.">>}), + mod_muc:room_destroyed(H, N, Pid, SH), + mod_muc:forget_room(SH, H, N); + +act_on_room(list, _, _) -> + ok. + + +%%---------------------------- +%% Change Room Option +%%---------------------------- + +get_room_occupants(Room, Host) -> + case get_room_pid(Room, Host) of + room_not_found -> throw({error, room_not_found}); + Pid -> get_room_occupants(Pid) + end. + +get_room_occupants(Pid) -> + S = get_room_state(Pid), + lists:map( + fun({_LJID, Info}) -> + {jlib:jid_to_string(Info#user.jid), + Info#user.nick, + atom_to_list(Info#user.role)} + end, + dict:to_list(S#state.users)). + +get_room_occupants_number(Room, Host) -> + length(get_room_occupants(Room, Host)). + +%%---------------------------- +%% Send Direct Invitation +%%---------------------------- +%% http://xmpp.org/extensions/xep-0249.html + +send_direct_invitation(RoomString, Password, Reason, UsersString) -> + RoomJid = jlib:string_to_jid(RoomString), + XmlEl = build_invitation(Password, Reason, RoomString), + UsersStrings = get_users_to_invite(RoomJid, binary_to_list(UsersString)), + [send_direct_invitation(RoomJid, jlib:string_to_jid(list_to_binary(UserStrings)), XmlEl) + || UserStrings <- UsersStrings], + timer:sleep(1000), + ok. + +get_users_to_invite(RoomJid, UsersString) -> + UsersStrings = string:tokens(UsersString, ":"), + OccupantsTuples = get_room_occupants(RoomJid#jid.luser, + RoomJid#jid.lserver), + OccupantsJids = [jlib:string_to_jid(JidString) + || {JidString, _Nick, _} <- OccupantsTuples], + lists:filter( + fun(UserString) -> + UserJid = jlib:string_to_jid(list_to_binary(UserString)), + %% [{"badlop@localhost/work","badlop","moderator"}] + lists:all(fun(OccupantJid) -> + UserJid#jid.luser /= OccupantJid#jid.luser + orelse UserJid#jid.lserver /= OccupantJid#jid.lserver + end, + OccupantsJids) + end, + UsersStrings). + +build_invitation(Password, Reason, RoomString) -> + PasswordAttrList = case Password of + <<"none">> -> []; + _ -> [{<<"password">>, Password}] + end, + ReasonAttrList = case Reason of + <<"none">> -> []; + _ -> [{<<"reason">>, Reason}] + end, + XAttrs = [{<<"xmlns">>, ?NS_XCONFERENCE}, + {<<"jid">>, RoomString}] + ++ PasswordAttrList + ++ ReasonAttrList, + XEl = {xmlel, <<"x">>, XAttrs, []}, + {xmlel, <<"message">>, [], [XEl]}. + +send_direct_invitation(FromJid, UserJid, XmlEl) -> + ejabberd_router:route(FromJid, UserJid, XmlEl). + +%%---------------------------- +%% Change Room Option +%%---------------------------- + +%% @spec(Name::string(), Service::string(), Option::string(), Value) -> ok +%% Value = atom() | integer() | string() +%% @doc Change an option in an existing room. +%% Requires the name of the room, the MUC service where it exists, +%% the option to change (for example title or max_users), +%% and the value to assign to the new option. +%% For example: +%% change_room_option("testroom", "conference.localhost", "title", "Test Room") +change_room_option(Name, Service, Option, Value) when is_atom(Option) -> + Pid = get_room_pid(Name, Service), + {ok, _} = change_room_option(Pid, Option, Value), + ok; +change_room_option(Name, Service, OptionString, ValueString) -> + Option = jlib:binary_to_atom(OptionString), + Value = case Option of + title -> ValueString; + description -> ValueString; + password -> ValueString; + subject ->ValueString; + subject_author ->ValueString; + max_users -> jlib:binary_to_integer(ValueString); + _ -> jlib:binary_to_atom(ValueString) + end, + change_room_option(Name, Service, Option, Value). + +change_room_option(Pid, Option, Value) -> + Config = get_room_config(Pid), + Config2 = change_option(Option, Value, Config), + gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}). + +%% @doc Get the Pid of an existing MUC room, or 'room_not_found'. +get_room_pid(Name, Service) -> + case mnesia:dirty_read(muc_online_room, {Name, Service}) of + [] -> + room_not_found; + [Room] -> + Room#muc_online_room.pid + end. + +%% It is required to put explicitely all the options because +%% the record elements are replaced at compile time. +%% So, this can't be parametrized. +change_option(Option, Value, Config) -> + case Option of + allow_change_subj -> Config#config{allow_change_subj = Value}; + allow_private_messages -> Config#config{allow_private_messages = Value}; + allow_query_users -> Config#config{allow_query_users = Value}; + allow_user_invites -> Config#config{allow_user_invites = Value}; + anonymous -> Config#config{anonymous = Value}; + logging -> Config#config{logging = Value}; + max_users -> Config#config{max_users = Value}; + members_by_default -> Config#config{members_by_default = Value}; + members_only -> Config#config{members_only = Value}; + moderated -> Config#config{moderated = Value}; + password -> Config#config{password = Value}; + password_protected -> Config#config{password_protected = Value}; + persistent -> Config#config{persistent = Value}; + public -> Config#config{public = Value}; + public_list -> Config#config{public_list = Value}; + title -> Config#config{title = Value} + end. + + +%%---------------------------- +%% Get Room Affiliations +%%---------------------------- + +%% @spec(Name::binary(), Service::binary()) -> +%% [{JID::string(), Domain::string(), Role::string(), Reason::string()}] +%% @doc Get the affiliations of the room Name@Service. +get_room_affiliations(Name, Service) -> + case mnesia:dirty_read(muc_online_room, {Name, Service}) of + [R] -> + %% Get the PID of the online room, then request its state + Pid = R#muc_online_room.pid, + {ok, StateData} = gen_fsm:sync_send_all_state_event(Pid, get_state), + Affiliations = ?DICT:to_list(StateData#state.affiliations), + lists:map( + fun({{Uname, Domain, _Res}, {Aff, Reason}}) when is_atom(Aff)-> + {Uname, Domain, Aff, Reason}; + ({{Uname, Domain, _Res}, Aff}) when is_atom(Aff)-> + {Uname, Domain, Aff, <<>>} + end, Affiliations); + [] -> + throw({error, "The room does not exist."}) + end. + +%%---------------------------- +%% Change Room Affiliation +%%---------------------------- + +%% @spec(Name, Service, JID, AffiliationString) -> ok | {error, Error} +%% Name = binary() +%% Service = binary() +%% JID = binary() +%% AffiliationString = "outcast" | "none" | "member" | "admin" | "owner" +%% @doc Set the affiliation of JID in the room Name@Service. +%% If the affiliation is 'none', the action is to remove, +%% In any other case the action will be to create the affiliation. +set_room_affiliation(Name, Service, JID, AffiliationString) -> + Affiliation = jlib:binary_to_atom(AffiliationString), + case mnesia:dirty_read(muc_online_room, {Name, Service}) of + [R] -> + %% Get the PID for the online room so we can get the state of the room + Pid = R#muc_online_room.pid, + {ok, StateData} = gen_fsm:sync_send_all_state_event(Pid, get_state), + SJID = jlib:string_to_jid(JID), + LJID = jlib:jid_remove_resource(jlib:jid_tolower(SJID)), + Affiliations = change_affiliation(Affiliation, LJID, StateData#state.affiliations), + Res = StateData#state{affiliations = Affiliations}, + {ok, _State} = gen_fsm:sync_send_all_state_event(Pid, {change_state, Res}), + mod_muc:store_room(Res#state.server_host, Res#state.host, Res#state.room, make_opts(Res)), + ok; + [] -> + error + end. + +change_affiliation(none, LJID, Affiliations) -> + ?DICT:erase(LJID, Affiliations); +change_affiliation(Affiliation, LJID, Affiliations) -> + ?DICT:store(LJID, Affiliation, Affiliations). + +-define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}). + +make_opts(StateData) -> + Config = StateData#state.config, + [ + ?MAKE_CONFIG_OPT(title), + ?MAKE_CONFIG_OPT(allow_change_subj), + ?MAKE_CONFIG_OPT(allow_query_users), + ?MAKE_CONFIG_OPT(allow_private_messages), + ?MAKE_CONFIG_OPT(public), + ?MAKE_CONFIG_OPT(public_list), + ?MAKE_CONFIG_OPT(persistent), + ?MAKE_CONFIG_OPT(moderated), + ?MAKE_CONFIG_OPT(members_by_default), + ?MAKE_CONFIG_OPT(members_only), + ?MAKE_CONFIG_OPT(allow_user_invites), + ?MAKE_CONFIG_OPT(password_protected), + ?MAKE_CONFIG_OPT(password), + ?MAKE_CONFIG_OPT(anonymous), + ?MAKE_CONFIG_OPT(logging), + ?MAKE_CONFIG_OPT(max_users), + {affiliations, ?DICT:to_list(StateData#state.affiliations)}, + {subject, StateData#state.subject}, + {subject_author, StateData#state.subject_author} + ]. + + +%%---------------------------- +%% Utils +%%---------------------------- + +uptime_seconds() -> + trunc(element(1, erlang:statistics(wall_clock))/1000). + +find_host(global) -> + global; +find_host("global") -> + global; +find_host(<<"global">>) -> + global; +find_host(ServerHost) when is_list(ServerHost) -> + find_host(list_to_binary(ServerHost)); +find_host(ServerHost) -> + gen_mod:get_module_opt_host(ServerHost, mod_muc, <<"conference.@HOST@">>). diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl index 626cf745e..d94151418 100644 --- a/src/mod_muc_log.erl +++ b/src/mod_muc_log.erl @@ -381,6 +381,11 @@ set_filemode(Fn, {FileMode, FileGroup}) -> ok = file:change_mode(Fn, list_to_integer(integer_to_list(FileMode), 8)), ok = file:change_group(Fn, FileGroup). +htmlize_nick(Nick1, html) -> + htmlize(<<"<", Nick1/binary, ">">>, html); +htmlize_nick(Nick1, plaintext) -> + htmlize(<<?PLAINTEXT_IN/binary, Nick1/binary, ?PLAINTEXT_OUT/binary>>, plaintext). + add_message_to_log(Nick1, Message, RoomJID, Opts, State) -> #logstate{out_dir = OutDir, dir_type = DirType, @@ -391,7 +396,7 @@ add_message_to_log(Nick1, Message, RoomJID, Opts, State, Room = get_room_info(RoomJID, Opts), Nick = htmlize(Nick1, FileFormat), - Nick2 = htmlize(<<"<", Nick1/binary, ">">>, FileFormat), + Nick2 = htmlize_nick(Nick1, FileFormat), Now = now(), TimeStamp = case Timezone of local -> calendar:now_to_local_time(Now); @@ -1245,6 +1250,5 @@ calc_hour_offset(TimeHere) -> 3600, TimeHereHour - TimeZeroHour. -fjoin([]) -> <<"/">>; fjoin(FileList) -> list_to_binary(filename:join([binary_to_list(File) || File <- FileList])). diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index e9092d4f8..ad77ae661 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -681,14 +681,11 @@ handle_event({service_message, Msg}, _StateName, children = [#xmlel{name = <<"body">>, attrs = [], children = [{xmlcdata, Msg}]}]}, - lists:foreach( - fun({_LJID, Info}) -> - ejabberd_router:route( - StateData#state.jid, - Info#user.jid, - MessagePkt) - end, - ?DICT:to_list(StateData#state.users)), + send_multiple( + StateData#state.jid, + StateData#state.server_host, + StateData#state.users, + MessagePkt), NSD = add_message_to_history(<<"">>, StateData#state.jid, MessagePkt, StateData), {next_state, normal_state, NSD}; @@ -752,6 +749,9 @@ handle_sync_event({change_config, Config}, _From, handle_sync_event({change_state, NewStateData}, _From, StateName, _StateData) -> {reply, {ok, NewStateData}, StateName, NewStateData}; +handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) -> + NSD = process_item_change(Item, StateData, UJID), + {reply, {ok, NSD}, StateName, NSD}; handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. @@ -942,16 +942,11 @@ process_groupchat_message(From, end, case IsAllowed of true -> - lists:foreach( - fun({_LJID, Info}) -> - ejabberd_router:route( - jlib:jid_replace_resource( - StateData#state.jid, - FromNick), - Info#user.jid, - Packet) - end, - ?DICT:to_list(StateData#state.users)), + send_multiple( + jlib:jid_replace_resource(StateData#state.jid, FromNick), + StateData#state.server_host, + StateData#state.users, + Packet), NewStateData2 = case has_body_or_subject(Packet) of true -> add_message_to_history(FromNick, From, @@ -2439,13 +2434,20 @@ add_message_to_history(FromNick, FromJID, Packet, StateData) -> _ -> true end, TimeStamp = now(), - SenderJid = case - (StateData#state.config)#config.anonymous - of - true -> StateData#state.jid; - false -> FromJID - end, - TSPacket = jlib:add_delay_info(Packet, SenderJid, TimeStamp), + AddrPacket = case (StateData#state.config)#config.anonymous of + true -> Packet; + false -> + Address = #xmlel{name = <<"address">>, + attrs = [{<<"type">>, <<"ofrom">>}, + {<<"jid">>, + jlib:jid_to_string(FromJID)}], + children = []}, + Addresses = #xmlel{name = <<"addresses">>, + attrs = [{<<"xmlns">>, ?NS_ADDRESS}], + children = [Address]}, + xml:append_subtags(Packet, [Addresses]) + end, + TSPacket = jlib:add_delay_info(AddrPacket, StateData#state.jid, TimeStamp), SPacket = jlib:replace_from_to(jlib:jid_replace_resource(StateData#state.jid, FromNick), @@ -2614,114 +2616,7 @@ process_admin_items_set(UJID, Items, Lang, StateData) -> "room ~s:~n ~p", [jlib:jid_to_string(UJID), jlib:jid_to_string(StateData#state.jid), Res]), - NSD = lists:foldl(fun (E, SD) -> - case catch case E of - {JID, affiliation, owner, _} - when JID#jid.luser == - <<"">> -> - %% If the provided JID does not have username, - %% forget the affiliation completely - SD; - {JID, role, none, Reason} -> - catch - send_kickban_presence(UJID, JID, - Reason, - <<"307">>, - SD), - set_role(JID, none, SD); - {JID, affiliation, none, - Reason} -> - case - (SD#state.config)#config.members_only - of - true -> - catch - send_kickban_presence(UJID, JID, - Reason, - <<"321">>, - none, - SD), - SD1 = - set_affiliation(JID, - none, - SD), - set_role(JID, none, - SD1); - _ -> - SD1 = - set_affiliation(JID, - none, - SD), - send_update_presence(JID, - SD1), - SD1 - end; - {JID, affiliation, outcast, - Reason} -> - catch - send_kickban_presence(UJID, JID, - Reason, - <<"301">>, - outcast, - SD), - set_affiliation(JID, - outcast, - set_role(JID, - none, - SD), - Reason); - {JID, affiliation, A, Reason} - when (A == admin) or - (A == owner) -> - SD1 = set_affiliation(JID, - A, - SD, - Reason), - SD2 = set_role(JID, - moderator, - SD1), - send_update_presence(JID, - Reason, - SD2), - SD2; - {JID, affiliation, member, - Reason} -> - SD1 = set_affiliation(JID, - member, - SD, - Reason), - SD2 = set_role(JID, - participant, - SD1), - send_update_presence(JID, - Reason, - SD2), - SD2; - {JID, role, Role, Reason} -> - SD1 = set_role(JID, Role, - SD), - catch - send_new_presence(JID, - Reason, - SD1), - SD1; - {JID, affiliation, A, - _Reason} -> - SD1 = set_affiliation(JID, - A, - SD), - send_update_presence(JID, - SD1), - SD1 - end - of - {'EXIT', ErrReason} -> - ?ERROR_MSG("MUC ITEMS SET ERR: ~p~n", - [ErrReason]), - SD; - NSD -> NSD - end - end, + NSD = lists:foldl(process_item_change(UJID), StateData, lists:flatten(Res)), case (NSD#state.config)#config.persistent of true -> @@ -2734,6 +2629,79 @@ process_admin_items_set(UJID, Items, Lang, StateData) -> Err -> Err end. +process_item_change(UJID) -> + fun(E, SD) -> + process_item_change(E, SD, UJID) + end. + +process_item_change(E, SD, UJID) -> + case catch case E of + {JID, affiliation, owner, _} when JID#jid.luser == <<"">> -> + %% If the provided JID does not have username, + %% forget the affiliation completely + SD; + {JID, role, none, Reason} -> + catch + send_kickban_presence(UJID, JID, + Reason, + <<"307">>, + SD), + set_role(JID, none, SD); + {JID, affiliation, none, Reason} -> + case (SD#state.config)#config.members_only of + true -> + catch + send_kickban_presence(UJID, JID, + Reason, + <<"321">>, + none, + SD), + SD1 = set_affiliation(JID, none, SD), + set_role(JID, none, SD1); + _ -> + SD1 = set_affiliation(JID, none, SD), + send_update_presence(JID, SD1), + SD1 + end; + {JID, affiliation, outcast, Reason} -> + catch + send_kickban_presence(UJID, JID, + Reason, + <<"301">>, + outcast, + SD), + set_affiliation(JID, + outcast, + set_role(JID, none, SD), + Reason); + {JID, affiliation, A, Reason} + when (A == admin) or (A == owner) -> + SD1 = set_affiliation(JID, A, SD, Reason), + SD2 = set_role(JID, moderator, SD1), + send_update_presence(JID, Reason, SD2), + SD2; + {JID, affiliation, member, Reason} -> + SD1 = set_affiliation(JID, member, SD, Reason), + SD2 = set_role(JID, participant, SD1), + send_update_presence(JID, Reason, SD2), + SD2; + {JID, role, Role, Reason} -> + SD1 = set_role(JID, Role, SD), + catch + send_new_presence(JID, Reason, SD1), + SD1; + {JID, affiliation, A, _Reason} -> + SD1 = set_affiliation(JID, A, SD), + send_update_presence(JID, SD1), + SD1 + end + of + {'EXIT', ErrReason} -> + ?ERROR_MSG("MUC ITEMS SET ERR: ~p~n", [ErrReason]), + SD; + NSD -> NSD + end. + find_changed_items(_UJID, _UAffiliation, _URole, [], _Lang, _StateData, Res) -> {result, Res}; @@ -4526,3 +4494,10 @@ has_body_or_subject(Packet) -> (#xmlel{name = <<"subject">>}) -> false; (_) -> true end, Packet#xmlel.children). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Multicast + +send_multiple(From, Server, Users, Packet) -> + JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], + ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl new file mode 100644 index 000000000..8a1960088 --- /dev/null +++ b/src/mod_multicast.erl @@ -0,0 +1,1162 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_multicast.erl +%%% Author : Badlop <badlop@process-one.net> +%%% Purpose : Extended Stanza Addressing (XEP-0033) support +%%% Created : 29 May 2007 by Badlop <badlop@process-one.net> +%%%---------------------------------------------------------------------- + +-module(mod_multicast). + +-author('badlop@process-one.net'). + +-behaviour(gen_server). + +-behaviour(gen_mod). + +%% API +-export([start_link/2, start/2, stop/1]). + +%% gen_server callbacks +-export([init/1, handle_info/2, handle_call/3, + handle_cast/2, terminate/2, code_change/3]). + +-export([purge_loop/1]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). + +-include("jlib.hrl"). + +-record(state, + {lserver, lservice, access, service_limits}). + +-record(multicastc, {rserver, response, ts}). + +%% ts: timestamp (in seconds) when the cache item was last updated + +-record(dest, {jid_string, jid_jid, type, full_xml}). + +%% jid_string = string() +%% jid_jid = jid() +%% full_xml = xml() + +-record(group, + {server, dests, multicast, others, addresses}). + +%% server = string() +%% dests = [string()] +%% multicast = {cached, local_server} | {cached, string()} | {cached, not_supported} | {obsolete, not_supported} | {obsolete, string()} | not_cached +%% after being updated, possible values are: local | multicast_not_supported | {multicast_supported, string(), limits()} +%% others = [xml()] +%% packet = xml() + +-record(waiter, + {awaiting, group, renewal = false, sender, packet, + aattrs, addresses}). + +%% awaiting = {[Remote_service], Local_service, Type_awaiting} +%% Remote_service = Local_service = string() +%% Type_awaiting = info | items +%% group = #group +%% renewal = true | false +%% sender = From +%% packet = xml() +%% aattrs = [xml()] + +-record(limits, {message, presence}). + +%% message = presence = integer() | infinite + +-record(service_limits, {local, remote}). + +%% All the elements are of type value() + +-define(VERSION_MULTICAST, <<"$Revision: 440 $ ">>). + +-define(PROCNAME, ejabberd_mod_multicast). + +-define(PURGE_PROCNAME, + ejabberd_mod_multicast_purgeloop). + +-define(MAXTIME_CACHE_POSITIVE, 86400). + +-define(MAXTIME_CACHE_NEGATIVE, 86400). + +-define(CACHE_PURGE_TIMER, 86400000). + +-define(DISCO_QUERY_TIMEOUT, 10000). + +-define(DEFAULT_LIMIT_LOCAL_MESSAGE, 100). + +-define(DEFAULT_LIMIT_LOCAL_PRESENCE, 100). + +-define(DEFAULT_LIMIT_REMOTE_MESSAGE, 20). + +-define(DEFAULT_LIMIT_REMOTE_PRESENCE, 20). + +start_link(LServerS, Opts) -> + Proc = gen_mod:get_module_proc(LServerS, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, + [LServerS, Opts], []). + +start(LServerS, Opts) -> + Proc = gen_mod:get_module_proc(LServerS, ?PROCNAME), + ChildSpec = {Proc, + {?MODULE, start_link, [LServerS, Opts]}, temporary, + 1000, worker, [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(LServerS) -> + Proc = gen_mod:get_module_proc(LServerS, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:terminate_child(ejabberd_sup, Proc), + supervisor:delete_child(ejabberd_sup, Proc). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +init([LServerS, Opts]) -> + LServiceS = gen_mod:get_opt_host(LServerS, Opts, + <<"multicast.@HOST@">>), + Access = gen_mod:get_opt(access, Opts, + fun (A) when is_atom(A) -> A end, all), + SLimits = + build_service_limit_record(gen_mod:get_opt(limits, Opts, + fun (A) when is_list(A) -> + A + end, + [])), + create_cache(), + try_start_loop(), + create_pool(), + ejabberd_router_multicast:register_route(LServerS), + ejabberd_router:register_route(LServiceS), + {ok, + #state{lservice = LServiceS, lserver = LServerS, + access = Access, service_limits = SLimits}}. + +handle_call(stop, _From, State) -> + try_stop_loop(), {stop, normal, ok, State}. + +handle_cast(_Msg, State) -> {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- + +handle_info({route, From, To, + #xmlel{name = <<"iq">>, attrs = Attrs} = Packet}, + State) -> + IQ = jlib:iq_query_info(Packet), + case catch process_iq(From, IQ, State) of + Result when is_record(Result, iq) -> + ejabberd_router:route(To, From, jlib:iq_to_xml(Result)); + {'EXIT', Reason} -> + ?ERROR_MSG("Error when processing IQ stanza: ~p", + [Reason]), + Err = jlib:make_error_reply(Packet, + ?ERR_INTERNAL_SERVER_ERROR), + ejabberd_router:route(To, From, Err); + reply -> + LServiceS = jts(To), + case xml:get_attr_s(<<"type">>, Attrs) of + <<"result">> -> + process_iqreply_result(From, LServiceS, Packet, State); + <<"error">> -> + process_iqreply_error(From, LServiceS, Packet) + end + end, + {noreply, State}; +%% XEP33 allows only 'message' and 'presence' stanza type +handle_info({route, From, To, + #xmlel{name = Stanza_type} = Packet}, + #state{lservice = LServiceS, lserver = LServerS, + access = Access, service_limits = SLimits} = + State) + when (Stanza_type == <<"message">>) or + (Stanza_type == <<"presence">>) -> + route_untrusted(LServiceS, LServerS, Access, SLimits, + From, To, Packet), + {noreply, State}; +%% Handle multicast packets sent by trusted local services +handle_info({route_trusted, From, Destinations, Packet}, + #state{lservice = LServiceS, lserver = LServerS} = + State) -> + route_trusted(LServiceS, LServerS, From, Destinations, + Packet), + {noreply, State}; +handle_info({get_host, Pid}, State) -> + Pid ! {my_host, State#state.lservice}, {noreply, State}; +handle_info(_Info, State) -> {noreply, State}. + +terminate(_Reason, State) -> + ejabberd_router_multicast:unregister_route(State#state.lserver), + ejabberd_router:unregister_route(State#state.lservice), + ok. + +code_change(_OldVsn, State, _Extra) -> {ok, State}. + +%%==================================================================== +%%% Internal functions +%%==================================================================== + +%%%------------------------ +%%% IQ Request Processing +%%%------------------------ + +process_iq(From, + #iq{type = get, xmlns = ?NS_DISCO_INFO, lang = Lang} = + IQ, + State) -> + IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}], + children = iq_disco_info(From, Lang, State)}]}; +%% disco#items request +process_iq(_, + #iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ, _) -> + IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, ?NS_DISCO_ITEMS}], + children = []}]}; +%% vCard request +process_iq(_, + #iq{type = get, xmlns = ?NS_VCARD, lang = Lang} = IQ, + _) -> + IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"vCard">>, + attrs = [{<<"xmlns">>, ?NS_VCARD}], + children = iq_vcard(Lang)}]}; +%% Unknown "set" or "get" request +process_iq(_, #iq{type = Type, sub_el = SubEl} = IQ, _) + when Type == get; Type == set -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]}; +%% IQ "result" or "error". +process_iq(_, reply, _) -> reply; +%% IQ "result" or "error". +process_iq(_, _, _) -> ok. + +-define(FEATURE(Feat), + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, Feat}], children = []}). + +iq_disco_info(From, Lang, State) -> + [#xmlel{name = <<"identity">>, + attrs = + [{<<"category">>, <<"service">>}, + {<<"type">>, <<"multicast">>}, + {<<"name">>, + translate:translate(Lang, <<"Multicast">>)}], + children = []}, + ?FEATURE((?NS_DISCO_INFO)), ?FEATURE((?NS_DISCO_ITEMS)), + ?FEATURE((?NS_VCARD)), ?FEATURE((?NS_ADDRESS))] + ++ iq_disco_info_extras(From, State). + +iq_vcard(Lang) -> + [#xmlel{name = <<"FN">>, attrs = [], + children = [{xmlcdata, <<"ejabberd/mod_multicast">>}]}, + #xmlel{name = <<"URL">>, attrs = [], + children = [{xmlcdata, ?EJABBERD_URI}]}, + #xmlel{name = <<"DESC">>, attrs = [], + children = + [{xmlcdata, + <<(translate:translate(Lang, + <<"ejabberd Multicast service">>))/binary, + "\nCopyright (c) 2002-2015 ProcessOne">>}]}]. + +%%%------------------------- +%%% Route +%%%------------------------- + +route_trusted(LServiceS, LServerS, FromJID, + Destinations, Packet) -> + Packet_stripped = Packet, + AAttrs = [{<<"xmlns">>, ?NS_ADDRESS}], + Delivereds = [], + Dests2 = lists:map(fun (D) -> + DS = jts(D), + XML = #xmlel{name = <<"address">>, + attrs = + [{<<"type">>, <<"bcc">>}, + {<<"jid">>, DS}], + children = []}, + #dest{jid_string = DS, jid_jid = D, + type = <<"bcc">>, full_xml = XML} + end, + Destinations), + Groups = group_dests(Dests2), + route_common(LServerS, LServiceS, FromJID, Groups, + Delivereds, Packet_stripped, AAttrs). + +route_untrusted(LServiceS, LServerS, Access, SLimits, + From, To, Packet) -> + try route_untrusted2(LServiceS, LServerS, Access, + SLimits, From, Packet) + catch + adenied -> + route_error(To, From, Packet, forbidden, + <<"Access denied by service policy">>); + eadsele -> + route_error(To, From, Packet, bad_request, + <<"No addresses element found">>); + eadeles -> + route_error(To, From, Packet, bad_request, + <<"No address elements found">>); + ewxmlns -> + route_error(To, From, Packet, bad_request, + <<"Wrong xmlns">>); + etoorec -> + route_error(To, From, Packet, not_acceptable, + <<"Too many receiver fields were specified">>); + edrelay -> + route_error(To, From, Packet, forbidden, + <<"Packet relay is denied by service policy">>); + EType:EReason -> + ?ERROR_MSG("Multicast unknown error: Type: ~p~nReason: ~p", + [EType, EReason]), + route_error(To, From, Packet, internal_server_error, + <<"Unknown problem">>) + end. + +route_untrusted2(LServiceS, LServerS, Access, SLimits, + FromJID, Packet) -> + ok = check_access(LServerS, Access, FromJID), + {ok, Packet_stripped, AAttrs, Addresses} = + strip_addresses_element(Packet), + {To_deliver, Delivereds} = + split_addresses_todeliver(Addresses), + Dests = convert_dest_record(To_deliver), + {Dests2, Not_jids} = split_dests_jid(Dests), + report_not_jid(FromJID, Packet, Not_jids), + ok = check_limit_dests(SLimits, FromJID, Packet, + Dests2), + Groups = group_dests(Dests2), + ok = check_relay(FromJID#jid.server, LServerS, Groups), + route_common(LServerS, LServiceS, FromJID, Groups, + Delivereds, Packet_stripped, AAttrs). + +route_common(LServerS, LServiceS, FromJID, Groups, + Delivereds, Packet_stripped, AAttrs) -> + Groups2 = look_cached_servers(LServerS, Groups), + Groups3 = build_others_xml(Groups2), + Groups4 = add_addresses(Delivereds, Groups3), + AGroups = decide_action_groups(Groups4), + act_groups(FromJID, Packet_stripped, AAttrs, LServiceS, + AGroups). + +act_groups(FromJID, Packet_stripped, AAttrs, LServiceS, + AGroups) -> + [perform(FromJID, Packet_stripped, AAttrs, LServiceS, + AGroup) + || AGroup <- AGroups]. + +perform(From, Packet, AAttrs, _, + {route_single, Group}) -> + [route_packet(From, ToUser, Packet, AAttrs, + Group#group.addresses) + || ToUser <- Group#group.dests]; +perform(From, Packet, AAttrs, _, + {{route_multicast, JID, RLimits}, Group}) -> + route_packet_multicast(From, JID, Packet, AAttrs, + Group#group.dests, Group#group.addresses, RLimits); +perform(From, Packet, AAttrs, LServiceS, + {{ask, Old_service, renewal}, Group}) -> + send_query_info(Old_service, LServiceS), + add_waiter(#waiter{awaiting = + {[Old_service], LServiceS, info}, + group = Group, renewal = true, sender = From, + packet = Packet, aattrs = AAttrs, + addresses = Group#group.addresses}); +perform(From, Packet, AAttrs, LServiceS, + {{ask, Server, not_renewal}, Group}) -> + send_query_info(Server, LServiceS), + add_waiter(#waiter{awaiting = + {[Server], LServiceS, info}, + group = Group, renewal = false, sender = From, + packet = Packet, aattrs = AAttrs, + addresses = Group#group.addresses}). + +%%%------------------------- +%%% Check access permission +%%%------------------------- + +check_access(LServerS, Access, From) -> + case acl:match_rule(LServerS, Access, From) of + allow -> ok; + _ -> throw(adenied) + end. + +%%%------------------------- +%%% Strip 'addresses' XML element +%%%------------------------- + +strip_addresses_element(Packet) -> + case xml:get_subtag(Packet, <<"addresses">>) of + #xmlel{name = <<"addresses">>, attrs = AAttrs, + children = Addresses} -> + case xml:get_attr_s(<<"xmlns">>, AAttrs) of + ?NS_ADDRESS -> + #xmlel{name = Name, attrs = Attrs, children = Els} = + Packet, + Els_stripped = lists:keydelete(<<"addresses">>, 2, Els), + Packet_stripped = #xmlel{name = Name, attrs = Attrs, + children = Els_stripped}, + {ok, Packet_stripped, AAttrs, Addresses}; + _ -> throw(ewxmlns) + end; + _ -> throw(eadsele) + end. + +%%%------------------------- +%%% Split Addresses +%%%------------------------- + +split_addresses_todeliver(Addresses) -> + lists:partition(fun (XML) -> + case XML of + #xmlel{name = <<"address">>, attrs = Attrs} -> + case xml:get_attr_s(<<"delivered">>, Attrs) of + <<"true">> -> false; + _ -> + Type = xml:get_attr_s(<<"type">>, + Attrs), + case Type of + <<"to">> -> true; + <<"cc">> -> true; + <<"bcc">> -> true; + _ -> false + end + end; + _ -> false + end + end, + Addresses). + +%%%------------------------- +%%% Check does not exceed limit of destinations +%%%------------------------- + +check_limit_dests(SLimits, FromJID, Packet, + Addresses) -> + SenderT = sender_type(FromJID), + Limits = get_slimit_group(SenderT, SLimits), + Type_of_stanza = type_of_stanza(Packet), + {_Type, Limit_number} = get_limit_number(Type_of_stanza, + Limits), + case length(Addresses) > Limit_number of + false -> ok; + true -> throw(etoorec) + end. + +%%%------------------------- +%%% Convert Destination XML to record +%%%------------------------- + +convert_dest_record(XMLs) -> + lists:map(fun (XML) -> + case xml:get_tag_attr_s(<<"jid">>, XML) of + <<"">> -> #dest{jid_string = none, full_xml = XML}; + JIDS -> + Type = xml:get_tag_attr_s(<<"type">>, XML), + JIDJ = stj(JIDS), + #dest{jid_string = JIDS, jid_jid = JIDJ, + type = Type, full_xml = XML} + end + end, + XMLs). + +%%%------------------------- +%%% Split destinations by existence of JID +%%% and send error messages for other dests +%%%------------------------- + +split_dests_jid(Dests) -> + lists:partition(fun (Dest) -> + case Dest#dest.jid_string of + none -> false; + _ -> true + end + end, + Dests). + +report_not_jid(From, Packet, Dests) -> + Dests2 = [xml:element_to_binary(Dest#dest.full_xml) + || Dest <- Dests], + [route_error(From, From, Packet, jid_malformed, + <<"This service can not process the address: ", + D/binary>>) + || D <- Dests2]. + +%%%------------------------- +%%% Group destinations by their servers +%%%------------------------- + +group_dests(Dests) -> + D = lists:foldl(fun (Dest, Dict) -> + ServerS = (Dest#dest.jid_jid)#jid.server, + dict:append(ServerS, Dest, Dict) + end, + dict:new(), Dests), + Keys = dict:fetch_keys(D), + [#group{server = Key, dests = dict:fetch(Key, D)} + || Key <- Keys]. + +%%%------------------------- +%%% Look for cached responses +%%%------------------------- + +look_cached_servers(LServerS, Groups) -> + [look_cached(LServerS, Group) || Group <- Groups]. + +look_cached(LServerS, G) -> + Maxtime_positive = (?MAXTIME_CACHE_POSITIVE), + Maxtime_negative = (?MAXTIME_CACHE_NEGATIVE), + Cached_response = search_server_on_cache(G#group.server, + LServerS, + {Maxtime_positive, + Maxtime_negative}), + G#group{multicast = Cached_response}. + +%%%------------------------- +%%% Build delivered XML element +%%%------------------------- + +build_others_xml(Groups) -> + [Group#group{others = + build_other_xml(Group#group.dests)} + || Group <- Groups]. + +build_other_xml(Dests) -> + lists:foldl(fun (Dest, R) -> + XML = Dest#dest.full_xml, + case Dest#dest.type of + <<"to">> -> [add_delivered(XML) | R]; + <<"cc">> -> [add_delivered(XML) | R]; + <<"bcc">> -> R; + _ -> [XML | R] + end + end, + [], Dests). + +add_delivered(#xmlel{name = Name, attrs = Attrs, + children = Els}) -> + Attrs2 = [{<<"delivered">>, <<"true">>} | Attrs], + #xmlel{name = Name, attrs = Attrs2, children = Els}. + +%%%------------------------- +%%% Add preliminary packets +%%%------------------------- + +add_addresses(Delivereds, Groups) -> + Ps = [Group#group.others || Group <- Groups], + add_addresses2(Delivereds, Groups, [], [], Ps). + +add_addresses2(_, [], Res, _, []) -> Res; +add_addresses2(Delivereds, [Group | Groups], Res, Pa, + [Pi | Pz]) -> + Addresses = lists:append([Delivereds] ++ Pa ++ Pz), + Group2 = Group#group{addresses = Addresses}, + add_addresses2(Delivereds, Groups, [Group2 | Res], + [Pi | Pa], Pz). + +%%%------------------------- +%%% Decide action groups +%%%------------------------- + +decide_action_groups(Groups) -> + [{decide_action_group(Group), Group} + || Group <- Groups]. + +decide_action_group(Group) -> + Server = Group#group.server, + case Group#group.multicast of + {cached, local_server} -> + %% Send a copy of the packet to each local user on Dests + route_single; + {cached, not_supported} -> + %% Send a copy of the packet to each remote user on Dests + route_single; + {cached, {multicast_supported, JID, RLimits}} -> + {route_multicast, JID, RLimits}; + {obsolete, + {multicast_supported, Old_service, _RLimits}} -> + {ask, Old_service, renewal}; + {obsolete, not_supported} -> {ask, Server, not_renewal}; + not_cached -> {ask, Server, not_renewal} + end. + +%%%------------------------- +%%% Route packet +%%%------------------------- + +route_packet(From, ToDest, Packet, AAttrs, Addresses) -> + Dests = case ToDest#dest.type of + <<"bcc">> -> []; + _ -> [ToDest] + end, + route_packet2(From, ToDest#dest.jid_string, Dests, + Packet, AAttrs, Addresses). + +route_packet_multicast(From, ToS, Packet, AAttrs, Dests, + Addresses, Limits) -> + Type_of_stanza = type_of_stanza(Packet), + {_Type, Limit_number} = get_limit_number(Type_of_stanza, + Limits), + Fragmented_dests = fragment_dests(Dests, Limit_number), + [route_packet2(From, ToS, DFragment, Packet, AAttrs, + Addresses) + || DFragment <- Fragmented_dests]. + +route_packet2(From, ToS, Dests, Packet, AAttrs, + Addresses) -> + #xmlel{name = T, attrs = A, children = C} = Packet, + C2 = case append_dests(Dests, Addresses) of + [] -> C; + ACs -> + [#xmlel{name = <<"addresses">>, attrs = AAttrs, + children = ACs} + | C] + end, + Packet2 = #xmlel{name = T, attrs = A, children = C2}, + ToJID = stj(ToS), + ejabberd_router:route(From, ToJID, Packet2). + +append_dests([], Addresses) -> Addresses; +append_dests([Dest | Dests], Addresses) -> + append_dests(Dests, [Dest#dest.full_xml | Addresses]). + +%%%------------------------- +%%% Check relay +%%%------------------------- + +check_relay(RS, LS, Gs) -> + case check_relay_required(RS, LS, Gs) of + false -> ok; + true -> throw(edrelay) + end. + +check_relay_required(RServer, LServerS, Groups) -> + case str:str(RServer, LServerS) > 0 of + true -> false; + false -> check_relay_required(LServerS, Groups) + end. + +check_relay_required(LServerS, Groups) -> + lists:any(fun (Group) -> Group#group.server /= LServerS + end, + Groups). + +%%%------------------------- +%%% Check protocol support: Send request +%%%------------------------- + +send_query_info(RServerS, LServiceS) -> + case str:str(RServerS, <<"echo.">>) of + 1 -> false; + _ -> send_query(RServerS, LServiceS, ?NS_DISCO_INFO) + end. + +send_query_items(RServerS, LServiceS) -> + send_query(RServerS, LServiceS, ?NS_DISCO_ITEMS). + +send_query(RServerS, LServiceS, XMLNS) -> + Packet = #xmlel{name = <<"iq">>, + attrs = [{<<"to">>, RServerS}, {<<"type">>, <<"get">>}], + children = + [#xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, XMLNS}], + children = []}]}, + ejabberd_router:route(stj(LServiceS), stj(RServerS), + Packet). + +%%%------------------------- +%%% Check protocol support: Receive response: Error +%%%------------------------- + +process_iqreply_error(From, LServiceS, _Packet) -> + FromS = jts(From), + case search_waiter(FromS, LServiceS, info) of + {found_waiter, Waiter} -> + received_awaiter(FromS, Waiter, LServiceS); + _ -> ok + end. + +%%%------------------------- +%%% Check protocol support: Receive response: Disco +%%%------------------------- + +process_iqreply_result(From, LServiceS, Packet, + State) -> + #xmlel{name = <<"query">>, attrs = Attrs2, + children = Els2} = + xml:get_subtag(Packet, <<"query">>), + case xml:get_attr_s(<<"xmlns">>, Attrs2) of + ?NS_DISCO_INFO -> + process_discoinfo_result(From, LServiceS, Els2, State); + ?NS_DISCO_ITEMS -> + process_discoitems_result(From, LServiceS, Els2) + end. + +%%%------------------------- +%%% Check protocol support: Receive response: Disco Info +%%%------------------------- + +process_discoinfo_result(From, LServiceS, Els, + _State) -> + FromS = jts(From), + case search_waiter(FromS, LServiceS, info) of + {found_waiter, Waiter} -> + process_discoinfo_result2(From, FromS, LServiceS, Els, + Waiter); + _ -> ok + end. + +process_discoinfo_result2(From, FromS, LServiceS, Els, + Waiter) -> + Multicast_support = lists:any(fun (XML) -> + case XML of + #xmlel{name = <<"feature">>, + attrs = Attrs} -> + (?NS_ADDRESS) == + xml:get_attr_s(<<"var">>, + Attrs); + _ -> false + end + end, + Els), + Group = Waiter#waiter.group, + RServer = Group#group.server, + case Multicast_support of + true -> + SenderT = sender_type(From), + RLimits = get_limits_xml(Els, SenderT), + add_response(RServer, + {multicast_supported, FromS, RLimits}), + FromM = Waiter#waiter.sender, + DestsM = Group#group.dests, + PacketM = Waiter#waiter.packet, + AAttrsM = Waiter#waiter.aattrs, + AddressesM = Waiter#waiter.addresses, + RServiceM = FromS, + route_packet_multicast(FromM, RServiceM, PacketM, + AAttrsM, DestsM, AddressesM, RLimits), + delo_waiter(Waiter); + false -> + case FromS of + RServer -> + send_query_items(FromS, LServiceS), + delo_waiter(Waiter), + add_waiter(Waiter#waiter{awaiting = + {[FromS], LServiceS, items}, + renewal = false}); + %% We asked a component, and it does not support XEP33 + _ -> received_awaiter(FromS, Waiter, LServiceS) + end + end. + +get_limits_xml(Els, SenderT) -> + LimitOpts = get_limits_els(Els), + build_remote_limit_record(LimitOpts, SenderT). + +get_limits_els(Els) -> + lists:foldl(fun (XML, R) -> + case XML of + #xmlel{name = <<"x">>, attrs = Attrs, + children = SubEls} -> + case ((?NS_XDATA) == + xml:get_attr_s(<<"xmlns">>, Attrs)) + and + (<<"result">> == + xml:get_attr_s(<<"type">>, Attrs)) + of + true -> get_limits_fields(SubEls) ++ R; + false -> R + end; + _ -> R + end + end, + [], Els). + +get_limits_fields(Fields) -> + {Head, Tail} = lists:partition(fun (Field) -> + case Field of + #xmlel{name = <<"field">>, + attrs = Attrs} -> + (<<"FORM_TYPE">> == + xml:get_attr_s(<<"var">>, + Attrs)) + and + (<<"hidden">> == + xml:get_attr_s(<<"type">>, + Attrs)); + _ -> false + end + end, + Fields), + case Head of + [] -> []; + _ -> get_limits_values(Tail) + end. + +get_limits_values(Values) -> + lists:foldl(fun (Value, R) -> + case Value of + #xmlel{name = <<"field">>, attrs = Attrs, + children = SubEls} -> + [#xmlel{name = <<"value">>, children = SubElsV}] = + SubEls, + Number = xml:get_cdata(SubElsV), + Name = xml:get_attr_s(<<"var">>, Attrs), + [{jlib:binary_to_atom(Name), + jlib:binary_to_integer(Number)} + | R]; + _ -> R + end + end, + [], Values). + +%%%------------------------- +%%% Check protocol support: Receive response: Disco Items +%%%------------------------- + +process_discoitems_result(From, LServiceS, Els) -> + List = lists:foldl(fun (XML, Res) -> + case XML of + #xmlel{name = <<"item">>, attrs = Attrs} -> + Res ++ [xml:get_attr_s(<<"jid">>, Attrs)]; + _ -> Res + end + end, + [], Els), + [send_query_info(Item, LServiceS) || Item <- List], + FromS = jts(From), + {found_waiter, Waiter} = search_waiter(FromS, LServiceS, + items), + delo_waiter(Waiter), + add_waiter(Waiter#waiter{awaiting = + {List, LServiceS, info}, + renewal = false}). + +%%%------------------------- +%%% Check protocol support: Receive response: Received awaiter +%%%------------------------- + +received_awaiter(JID, Waiter, LServiceS) -> + {JIDs, LServiceS, info} = Waiter#waiter.awaiting, + delo_waiter(Waiter), + Group = Waiter#waiter.group, + RServer = Group#group.server, + case lists:delete(JID, JIDs) of + [] -> + case Waiter#waiter.renewal of + false -> + add_response(RServer, not_supported), + From = Waiter#waiter.sender, + Packet = Waiter#waiter.packet, + AAttrs = Waiter#waiter.aattrs, + Addresses = Waiter#waiter.addresses, + [route_packet(From, ToUser, Packet, AAttrs, Addresses) + || ToUser <- Group#group.dests]; + true -> + send_query_info(RServer, LServiceS), + add_waiter(Waiter#waiter{awaiting = + {[RServer], LServiceS, info}, + renewal = false}) + end; + JIDs2 -> + add_waiter(Waiter#waiter{awaiting = + {JIDs2, LServiceS, info}, + renewal = false}) + end. + +%%%------------------------- +%%% Cache +%%%------------------------- + +create_cache() -> + mnesia:create_table(multicastc, + [{ram_copies, [node()]}, + {attributes, record_info(fields, multicastc)}]). + +add_response(RServer, Response) -> + Secs = + calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(now())), + mnesia:dirty_write(#multicastc{rserver = RServer, + response = Response, ts = Secs}). + +search_server_on_cache(RServer, LServerS, _Maxmins) + when RServer == LServerS -> + {cached, local_server}; +search_server_on_cache(RServer, _LServerS, Maxmins) -> + case look_server(RServer) of + not_cached -> not_cached; + {cached, Response, Ts} -> + Now = + calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(now())), + case is_obsolete(Response, Ts, Now, Maxmins) of + false -> {cached, Response}; + true -> {obsolete, Response} + end + end. + +look_server(RServer) -> + case mnesia:dirty_read(multicastc, RServer) of + [] -> not_cached; + [M] -> {cached, M#multicastc.response, M#multicastc.ts} + end. + +is_obsolete(Response, Ts, Now, {Max_pos, Max_neg}) -> + Max = case Response of + multicast_not_supported -> Max_neg; + _ -> Max_pos + end, + Now - Ts > Max. + +%%%------------------------- +%%% Purge cache +%%%------------------------- + +purge() -> + Maxmins_positive = (?MAXTIME_CACHE_POSITIVE), + Maxmins_negative = (?MAXTIME_CACHE_NEGATIVE), + Now = + calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(now())), + purge(Now, {Maxmins_positive, Maxmins_negative}). + +purge(Now, Maxmins) -> + F = fun () -> + mnesia:foldl(fun (R, _) -> + #multicastc{response = Response, ts = Ts} = + R, + case is_obsolete(Response, Ts, Now, + Maxmins) + of + true -> mnesia:delete_object(R); + false -> ok + end + end, + none, multicastc) + end, + mnesia:transaction(F). + +%%%------------------------- +%%% Purge cache loop +%%%------------------------- + +try_start_loop() -> + case lists:member(?PURGE_PROCNAME, registered()) of + true -> ok; + false -> start_loop() + end, + (?PURGE_PROCNAME) ! new_module. + +start_loop() -> + register(?PURGE_PROCNAME, + spawn(?MODULE, purge_loop, [0])), + (?PURGE_PROCNAME) ! purge_now. + +try_stop_loop() -> (?PURGE_PROCNAME) ! try_stop. + +purge_loop(NM) -> + receive + purge_now -> + purge(), + timer:send_after(?CACHE_PURGE_TIMER, ?PURGE_PROCNAME, + purge_now), + purge_loop(NM); + new_module -> purge_loop(NM + 1); + try_stop when NM > 1 -> purge_loop(NM - 1); + try_stop -> purge_loop_finished + end. + +%%%------------------------- +%%% Pool +%%%------------------------- + +create_pool() -> + catch ets:new(multicastp, + [duplicate_bag, public, named_table, {keypos, 2}]). + +add_waiter(Waiter) -> + true = ets:insert(multicastp, Waiter). + +delo_waiter(Waiter) -> + true = ets:delete_object(multicastp, Waiter). + +search_waiter(JID, LServiceS, Type) -> + Rs = ets:foldl(fun (W, Res) -> + {JIDs, LServiceS1, Type1} = W#waiter.awaiting, + case lists:member(JID, JIDs) and + (LServiceS == LServiceS1) + and (Type1 == Type) + of + true -> Res ++ [W]; + false -> Res + end + end, + [], multicastp), + case Rs of + [R | _] -> {found_waiter, R}; + [] -> waiter_not_found + end. + +%%%------------------------- +%%% Limits: utils +%%%------------------------- + +%% Type definitions for data structures related with XEP33 limits +%% limit() = {Name, Value} +%% Name = atom() +%% Value = {Type, Number} +%% Type = default | custom +%% Number = integer() | infinite + +list_of_limits(local) -> + [{message, ?DEFAULT_LIMIT_LOCAL_MESSAGE}, + {presence, ?DEFAULT_LIMIT_LOCAL_PRESENCE}]; +list_of_limits(remote) -> + [{message, ?DEFAULT_LIMIT_REMOTE_MESSAGE}, + {presence, ?DEFAULT_LIMIT_REMOTE_PRESENCE}]. + +build_service_limit_record(LimitOpts) -> + LimitOptsL = get_from_limitopts(LimitOpts, local), + LimitOptsR = get_from_limitopts(LimitOpts, remote), + {service_limits, build_limit_record(LimitOptsL, local), + build_limit_record(LimitOptsR, remote)}. + +get_from_limitopts(LimitOpts, SenderT) -> + [{StanzaT, Number} + || {SenderT2, StanzaT, Number} <- LimitOpts, + SenderT =:= SenderT2]. + +build_remote_limit_record(LimitOpts, SenderT) -> + build_limit_record(LimitOpts, SenderT). + +build_limit_record(LimitOpts, SenderT) -> + Limits = [get_limit_value(Name, Default, LimitOpts) + || {Name, Default} <- list_of_limits(SenderT)], + list_to_tuple([limits | Limits]). + +get_limit_value(Name, Default, LimitOpts) -> + case lists:keysearch(Name, 1, LimitOpts) of + {value, {Name, Number}} -> {custom, Number}; + false -> {default, Default} + end. + +type_of_stanza(#xmlel{name = <<"message">>}) -> message; +type_of_stanza(#xmlel{name = <<"presence">>}) -> + presence. + +get_limit_number(message, Limits) -> + Limits#limits.message; +get_limit_number(presence, Limits) -> + Limits#limits.presence. + +get_slimit_group(local, SLimits) -> + SLimits#service_limits.local; +get_slimit_group(remote, SLimits) -> + SLimits#service_limits.remote. + +fragment_dests(Dests, Limit_number) -> + {R, _} = lists:foldl(fun (Dest, {Res, Count}) -> + case Count of + Limit_number -> + Head2 = [Dest], {[Head2 | Res], 0}; + _ -> + [Head | Tail] = Res, + Head2 = [Dest | Head], + {[Head2 | Tail], Count + 1} + end + end, + {[[]], 0}, Dests), + R. + +%%%------------------------- +%%% Limits: XEP-0128 Service Discovery Extensions +%%%------------------------- + +%% Some parts of code are borrowed from mod_muc_room.erl + +-define(RFIELDT(Type, Var, Val), + #xmlel{name = <<"field">>, + attrs = [{<<"var">>, Var}, {<<"type">>, Type}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, Val}]}]}). + +-define(RFIELDV(Var, Val), + #xmlel{name = <<"field">>, attrs = [{<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, Val}]}]}). + +iq_disco_info_extras(From, State) -> + SenderT = sender_type(From), + Service_limits = State#state.service_limits, + case iq_disco_info_extras2(SenderT, Service_limits) of + [] -> []; + List_limits_xmpp -> + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}], + children = + [?RFIELDT(<<"hidden">>, <<"FORM_TYPE">>, (?NS_ADDRESS))] + ++ List_limits_xmpp}] + end. + +sender_type(From) -> + Local_hosts = (?MYHOSTS), + case lists:member(From#jid.lserver, Local_hosts) of + true -> local; + false -> remote + end. + +iq_disco_info_extras2(SenderT, SLimits) -> + Limits = get_slimit_group(SenderT, SLimits), + Stanza_types = [message, presence], + lists:foldl(fun (Type_of_stanza, R) -> + case get_limit_number(Type_of_stanza, Limits) of + {custom, Number} -> + [?RFIELDV((to_binary(Type_of_stanza)), + (to_binary(Number))) + | R]; + {default, _} -> R + end + end, + [], Stanza_types). + +to_binary(A) -> list_to_binary(hd(io_lib:format("~p", [A]))). + +%%%------------------------- +%%% Error report +%%%------------------------- + +route_error(From, To, Packet, ErrType, ErrText) -> + #xmlel{attrs = Attrs} = Packet, + Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + Reply = make_reply(ErrType, Lang, ErrText), + Err = jlib:make_error_reply(Packet, Reply), + ejabberd_router:route(From, To, Err). + +make_reply(bad_request, Lang, ErrText) -> + ?ERRT_BAD_REQUEST(Lang, ErrText); +make_reply(jid_malformed, Lang, ErrText) -> + ?ERRT_JID_MALFORMED(Lang, ErrText); +make_reply(not_acceptable, Lang, ErrText) -> + ?ERRT_NOT_ACCEPTABLE(Lang, ErrText); +make_reply(internal_server_error, Lang, ErrText) -> + ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText); +make_reply(forbidden, Lang, ErrText) -> + ?ERRT_FORBIDDEN(Lang, ErrText). + +stj(String) -> jlib:string_to_jid(String). + +jts(String) -> jlib:jid_to_string(String). diff --git a/src/mod_offline.erl b/src/mod_offline.erl index b0582bc20..7f9a81a0d 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -92,12 +92,13 @@ start_link(Host, Opts) -> start(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - temporary, 1000, worker, [?MODULE]}, + transient, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ?GEN_SERVER:call(Proc, stop), + catch ?GEN_SERVER:call(Proc, stop), + supervisor:terminate_child(ejabberd_sup, Proc), supervisor:delete_child(ejabberd_sup, Proc), ok. @@ -107,7 +108,7 @@ stop(Host) -> %%==================================================================== init([Host, Opts]) -> - case gen_mod:db_type(Opts) of + case gen_mod:db_type(Host, Opts) of mnesia -> mnesia:create_table(offline_msg, [{disc_only_copies, [node()]}, {type, bag}, @@ -235,7 +236,7 @@ store_offline_msg(Host, {User, _}, Msgs, Len, MaxOfflineMsgs, Len + count_offline_messages(User, Host); true -> 0 end, - if + if Count > MaxOfflineMsgs -> discard_warn_sender(Msgs); true -> @@ -560,7 +561,19 @@ remove_old_messages(Days, _LServer, mnesia) -> ok, offline_msg) end, mnesia:transaction(F); -remove_old_messages(_Days, _LServer, odbc) -> + +remove_old_messages(Days, LServer, odbc) -> + case catch ejabberd_odbc:sql_query( + LServer, + [<<"DELETE FROM spool" + " WHERE created_at < " + "DATE_SUB(CURDATE(), INTERVAL ">>, + integer_to_list(Days), <<" DAY);">>]) of + {updated, N} -> + ?INFO_MSG("~p message(s) deleted from offline spool", [N]); + _Error -> + ?ERROR_MSG("Cannot delete message in offline spool: ~p", [_Error]) + end, {atomic, ok}; remove_old_messages(_Days, _LServer, riak) -> {atomic, ok}. @@ -1044,10 +1057,7 @@ count_offline_messages(LUser, LServer, riak) -> Res; _ -> 0 - end; -count_offline_messages(_Acc, User, Server) -> - N = count_offline_messages(User, Server), - {stop, N}. + end. %% Return the number of records matching a given match expression. %% This function is intended to be used inside a Mnesia transaction. @@ -1085,14 +1095,10 @@ export(_Server) -> packet = Packet}) when LServer == Host -> Username = ejabberd_odbc:escape(LUser), - Packet1 = - jlib:replace_from_to(jlib:jid_to_string(From), - jlib:jid_to_string(To), Packet), - Packet2 = - jlib:add_delay_info(Packet1, LServer, TimeStamp, - <<"Offline Storage">>), - XML = - ejabberd_odbc:escape(xml:element_to_binary(Packet2)), + Packet1 = jlib:replace_from_to(From, To, Packet), + Packet2 = jlib:add_delay_info(Packet1, LServer, TimeStamp, + <<"Offline Storage">>), + XML = ejabberd_odbc:escape(xml:element_to_binary(Packet2)), [[<<"delete from spool where username='">>, Username, <<"';">>], [<<"insert into spool(username, xml) values ('">>, Username, <<"', '">>, XML, <<"');">>]]; diff --git a/src/mod_ping.erl b/src/mod_ping.erl index 87cf6e015..f493dccb8 100644 --- a/src/mod_ping.erl +++ b/src/mod_ping.erl @@ -63,7 +63,7 @@ send_pings = ?DEFAULT_SEND_PINGS :: boolean(), ping_interval = ?DEFAULT_PING_INTERVAL :: non_neg_integer(), timeout_action = none :: none | kill, - timers = (?DICT):new() :: dict()}). + timers = (?DICT):new() :: ?TDICT}). %%==================================================================== %% API diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl index c83a953c4..fd3f60247 100644 --- a/src/mod_privacy.erl +++ b/src/mod_privacy.erl @@ -58,7 +58,7 @@ privacy_schema() -> start(Host, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, one_queue), - case gen_mod:db_type(Opts) of + case gen_mod:db_type(Host, Opts) of mnesia -> mnesia:create_table(privacy, [{disc_copies, [node()]}, @@ -251,7 +251,7 @@ process_list_get(LUser, LServer, Name, odbc) -> <<"match_all">>, <<"match_iq">>, <<"match_message">>, <<"match_presence_in">>, <<"match_presence_out">>], RItems} -> - lists:map(fun raw_to_item/1, RItems); + lists:flatmap(fun raw_to_item/1, RItems); _ -> error end; _ -> error @@ -482,7 +482,7 @@ process_active_set(LUser, LServer, Name, odbc) -> <<"match_all">>, <<"match_iq">>, <<"match_message">>, <<"match_presence_in">>, <<"match_presence_out">>], RItems} -> - lists:map(fun raw_to_item/1, RItems); + lists:flatmap(fun raw_to_item/1, RItems); _ -> error end; _ -> error @@ -766,7 +766,7 @@ get_user_list(_, LUser, LServer, odbc) -> <<"match_all">>, <<"match_iq">>, <<"match_message">>, <<"match_presence_in">>, <<"match_presence_out">>], RItems} -> - {Default, lists:map(fun raw_to_item/1, RItems)}; + {Default, lists:flatmap(fun raw_to_item/1, RItems)}; _ -> {none, []} end; _ -> {none, []} @@ -813,7 +813,7 @@ get_user_lists(LUser, LServer, odbc) -> <<"match_message">>, <<"match_presence_in">>, <<"match_presence_out">>], RItems} -> - [{Name, lists:map(fun raw_to_item/1, RItems)}]; + [{Name, lists:flatmap(fun raw_to_item/1, RItems)}]; _ -> [] end @@ -967,39 +967,43 @@ updated_list(_, #userlist{name = OldName} = Old, raw_to_item([SType, SValue, SAction, SOrder, SMatchAll, SMatchIQ, SMatchMessage, SMatchPresenceIn, - SMatchPresenceOut]) -> - {Type, Value} = case SType of - <<"n">> -> {none, none}; - <<"j">> -> - case jlib:string_to_jid(SValue) of - #jid{} = JID -> {jid, jlib:jid_tolower(JID)} - end; - <<"g">> -> {group, SValue}; - <<"s">> -> - case SValue of - <<"none">> -> {subscription, none}; - <<"both">> -> {subscription, both}; - <<"from">> -> {subscription, from}; - <<"to">> -> {subscription, to} - end - end, - Action = case SAction of - <<"a">> -> allow; - <<"d">> -> deny - end, - Order = jlib:binary_to_integer(SOrder), - MatchAll = ejabberd_odbc:to_bool(SMatchAll), - MatchIQ = ejabberd_odbc:to_bool(SMatchIQ), - MatchMessage = ejabberd_odbc:to_bool(SMatchMessage), - MatchPresenceIn = - ejabberd_odbc:to_bool(SMatchPresenceIn), - MatchPresenceOut = - ejabberd_odbc:to_bool(SMatchPresenceOut), - #listitem{type = Type, value = Value, action = Action, - order = Order, match_all = MatchAll, match_iq = MatchIQ, - match_message = MatchMessage, - match_presence_in = MatchPresenceIn, - match_presence_out = MatchPresenceOut}. + SMatchPresenceOut] = Row) -> + try + {Type, Value} = case SType of + <<"n">> -> {none, none}; + <<"j">> -> + case jlib:string_to_jid(SValue) of + #jid{} = JID -> + {jid, jlib:jid_tolower(JID)} + end; + <<"g">> -> {group, SValue}; + <<"s">> -> + case SValue of + <<"none">> -> {subscription, none}; + <<"both">> -> {subscription, both}; + <<"from">> -> {subscription, from}; + <<"to">> -> {subscription, to} + end + end, + Action = case SAction of + <<"a">> -> allow; + <<"d">> -> deny + end, + Order = jlib:binary_to_integer(SOrder), + MatchAll = ejabberd_odbc:to_bool(SMatchAll), + MatchIQ = ejabberd_odbc:to_bool(SMatchIQ), + MatchMessage = ejabberd_odbc:to_bool(SMatchMessage), + MatchPresenceIn = ejabberd_odbc:to_bool(SMatchPresenceIn), + MatchPresenceOut = ejabberd_odbc:to_bool(SMatchPresenceOut), + [#listitem{type = Type, value = Value, action = Action, + order = Order, match_all = MatchAll, match_iq = MatchIQ, + match_message = MatchMessage, + match_presence_in = MatchPresenceIn, + match_presence_out = MatchPresenceOut}] + catch _:_ -> + ?WARNING_MSG("failed to parse row: ~p", [Row]), + [] + end. item_to_raw(#listitem{type = Type, value = Value, action = Action, order = Order, match_all = MatchAll, diff --git a/src/mod_private.erl b/src/mod_private.erl index cedcb2787..f09c6100e 100644 --- a/src/mod_private.erl +++ b/src/mod_private.erl @@ -49,7 +49,7 @@ start(Host, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, one_queue), - case gen_mod:db_type(Opts) of + case gen_mod:db_type(Host, Opts) of mnesia -> mnesia:create_table(private_storage, [{disc_only_copies, [node()]}, diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 3f4a4d7ec..579c47757 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -74,8 +74,7 @@ on_user_offline/3, remove_user/2, disco_local_identity/5, disco_local_features/5, disco_local_items/5, disco_sm_identity/5, - disco_sm_features/5, disco_sm_items/5, - drop_pep_error/4]). + disco_sm_features/5, disco_sm_items/5]). %% exported iq handlers -export([iq_sm/3]). @@ -87,7 +86,7 @@ unsubscribe_node/5, publish_item/6, delete_item/4, - send_items/6, + send_items/7, get_items/2, get_item/3, get_cached_item/2, @@ -345,8 +344,6 @@ init([ServerHost, Opts]) -> ?MODULE, disco_sm_features, 75), ejabberd_hooks:add(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75), - ejabberd_hooks:add(c2s_filter_packet_in, ServerHost, ?MODULE, - drop_pep_error, 75), gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB, ?MODULE, iq_sm, IQDisc), gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, @@ -864,6 +861,7 @@ send_loop(State) -> N, NodeId, Type, + Options, LJID, last); _ -> ok @@ -960,6 +958,7 @@ send_loop(State) -> Node, NodeId, Type, + Options, LJID, last); true -> @@ -1330,33 +1329,6 @@ unsubscribe_user(Entity, Owner) -> end). %% ------- -%% packet receive hook handling function -%% - -drop_pep_error(#xmlel{name = <<"message">>, attrs = Attrs} = Packet, _JID, From, - #jid{lresource = <<"">>} = To) -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> - case xml:get_subtag(Packet, <<"event">>) of - #xmlel{attrs = EventAttrs} -> - case xml:get_attr_s(<<"xmlns">>, EventAttrs) of - ?NS_PUBSUB_EVENT -> - ?DEBUG("Dropping PEP error message from ~s to ~s", - [jlib:jid_to_string(From), - jlib:jid_to_string(To)]), - drop; - _ -> - Packet - end; - false -> - Packet - end; - _ -> - Packet - end; -drop_pep_error(Acc, _JID, _From, _To) -> Acc. - -%% ------- %% user remove hook handling function %% @@ -1496,8 +1468,6 @@ terminate(_Reason, ?MODULE, disco_sm_features, 75), ejabberd_hooks:delete(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75), - ejabberd_hooks:delete(c2s_filter_packet_in, ServerHost, - ?MODULE, drop_pep_error, 75), gen_iq_handler:remove_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB), gen_iq_handler:remove_iq_handler(ejabberd_sm, @@ -2926,7 +2896,8 @@ subscribe_node(Host, Node, From, JID, Configuration) -> {TNode, {Result, subscribed, SubId, send_last}}} -> NodeId = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, - send_items(Host, Node, NodeId, Type, Subscriber, last), + Options = TNode#pubsub_node.options, + send_items(Host, Node, NodeId, Type, Options, Subscriber, last), case Result of default -> {result, Reply({subscribed, SubId})}; _ -> {result, Result} @@ -3388,14 +3359,15 @@ get_allowed_items_call(Host, NodeIdx, From, Type, Options, Owners) -> %% Node = pubsubNode() %% NodeId = pubsubNodeId() %% Type = pubsubNodeType() +%% Options = mod_pubsub:nodeOptions() %% LJID = {U, S, []} %% Number = last | integer() %% @doc <p>Resend the items of a node to the user.</p> %% @todo use cache-last-item feature -send_items(Host, Node, NodeId, Type, LJID, last) -> +send_items(Host, Node, NodeId, Type, Options, LJID, last) -> case get_cached_item(Host, NodeId) of undefined -> - send_items(Host, Node, NodeId, Type, LJID, 1); + send_items(Host, Node, NodeId, Type, Options, LJID, 1); LastItem -> {ModifNow, ModifUSR} = LastItem#pubsub_item.modification, @@ -3405,9 +3377,9 @@ send_items(Host, Node, NodeId, Type, LJID, last) -> children = itemsEls([LastItem])}], ModifNow, ModifUSR), - dispatch_items(Host, LJID, Node, Stanza) + dispatch_items(Host, LJID, Node, Options, Stanza) end; -send_items(Host, Node, NodeId, Type, LJID, Number) -> +send_items(Host, Node, NodeId, Type, Options, LJID, Number) -> ToSend = case node_action(Host, Type, get_items, [NodeId, LJID]) of @@ -3435,20 +3407,23 @@ send_items(Host, Node, NodeId, Type, LJID, Number) -> attrs = nodeAttr(Node), children = itemsEls(ToSend)}]) end, - dispatch_items(Host, LJID, Node, Stanza). + dispatch_items(Host, LJID, Node, Options, Stanza). --spec(dispatch_items/4 :: +-spec(dispatch_items/5 :: ( - From :: mod_pubsub:host(), - To :: jid(), - Node :: mod_pubsub:nodeId(), - Stanza :: xmlel() | undefined) + From :: mod_pubsub:host(), + To :: jid(), + Node :: mod_pubsub:nodeId(), + Options :: mod_pubsub:nodeOptions(), + Stanza :: xmlel() | undefined) -> any() ). -dispatch_items(_From, _To, _Node, _Stanza = undefined) -> ok; +dispatch_items(_From, _To, _Node, _Options, _Stanza = undefined) -> ok; dispatch_items({FromU, FromS, FromR} = From, {ToU, ToS, ToR} = To, Node, - Stanza) -> + Options, BaseStanza) -> + NotificationType = get_option(Options, notification_type, headline), + Stanza = add_message_type(BaseStanza, NotificationType), C2SPid = case ejabberd_sm:get_session_pid(ToU, ToS, ToR) of ToPid when is_pid(ToPid) -> ToPid; _ -> @@ -3465,7 +3440,9 @@ dispatch_items({FromU, FromS, FromR} = From, {ToU, ToS, ToR} = To, Node, service_jid(From), jlib:make_jid(To), Stanza) end; -dispatch_items(From, To, _Node, Stanza) -> +dispatch_items(From, To, _Node, Options, BaseStanza) -> + NotificationType = get_option(Options, notification_type, headline), + Stanza = add_message_type(BaseStanza, NotificationType), ejabberd_router:route(service_jid(From), jlib:make_jid(To), Stanza). %% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response} @@ -4485,10 +4462,7 @@ broadcast_stanza(Host, _Node, _NodeId, _Type, NodeOptions, SubsByDepth, NotifyTy NotificationType = get_option(NodeOptions, notification_type, headline), BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but usefull From = service_jid(Host), - Stanza = case NotificationType of - normal -> BaseStanza; - MsgType -> add_message_type(BaseStanza, iolist_to_binary(atom_to_list(MsgType))) - end, + Stanza = add_message_type(BaseStanza, NotificationType), %% Handles explicit subscriptions SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth), lists:foreach(fun ({LJID, NodeName, SubIDs}) -> @@ -4520,10 +4494,8 @@ broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, NodeId, Type, Nod SenderResource = user_resource(LUser, LServer, LResource), case ejabberd_sm:get_session_pid(LUser, LServer, SenderResource) of C2SPid when is_pid(C2SPid) -> - Stanza = case get_option(NodeOptions, notification_type, headline) of - normal -> BaseStanza; - MsgType -> add_message_type(BaseStanza, iolist_to_binary(atom_to_list(MsgType))) - end, + NotificationType = get_option(NodeOptions, notification_type, headline), + Stanza = add_message_type(BaseStanza, NotificationType), %% set the from address on the notification to the bare JID of the account owner %% Also, add "replyto" if entity has presence subscription to the account owner %% See XEP-0163 1.1 section 4.3.1 @@ -5301,10 +5273,19 @@ itemsEls(Items) -> #xmlel{name = <<"item">>, attrs = itemAttr(ItemId), children = Payload} end, Items). +-spec(add_message_type/2 :: +( + Message :: xmlel(), + Type :: atom()) + -> xmlel() +). + +add_message_type(Message, normal) -> Message; add_message_type(#xmlel{name = <<"message">>, attrs = Attrs, children = Els}, Type) -> #xmlel{name = <<"message">>, - attrs = [{<<"type">>, Type} | Attrs], children = Els}; + attrs = [{<<"type">>, jlib:atom_to_binary(Type)} | Attrs], + children = Els}; add_message_type(XmlEl, _Type) -> XmlEl. %% Place of <headers/> changed at the bottom of the stanza diff --git a/src/mod_pubsub_odbc.erl b/src/mod_pubsub_odbc.erl index 3b8ae682a..8b32b83e2 100644 --- a/src/mod_pubsub_odbc.erl +++ b/src/mod_pubsub_odbc.erl @@ -74,8 +74,7 @@ on_user_offline/3, remove_user/2, disco_local_identity/5, disco_local_features/5, disco_local_items/5, disco_sm_identity/5, - disco_sm_features/5, disco_sm_items/5, - drop_pep_error/4]). + disco_sm_features/5, disco_sm_items/5]). %% exported iq handlers -export([iq_sm/3]). @@ -87,7 +86,7 @@ unsubscribe_node/5, publish_item/6, delete_item/4, - send_items/6, + send_items/7, get_items/2, get_item/3, get_cached_item/2, @@ -345,8 +344,6 @@ init([ServerHost, Opts]) -> ?MODULE, disco_sm_features, 75), ejabberd_hooks:add(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75), - ejabberd_hooks:add(c2s_filter_packet_in, ServerHost, ?MODULE, - drop_pep_error, 75), gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB, ?MODULE, iq_sm, IQDisc), gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, @@ -464,12 +461,16 @@ send_loop(State) -> type = Type, id = - NodeId} = + NodeId, + options + = + Options} = Node, send_items(H, N, NodeId, Type, + Options, LJID, last); true -> @@ -564,6 +565,7 @@ send_loop(State) -> Node, NodeId, Type, + Options, LJID, last); true -> @@ -937,33 +939,6 @@ unsubscribe_user(Entity, Owner) -> end). %% ------- -%% packet receive hook handling function -%% - -drop_pep_error(#xmlel{name = <<"message">>, attrs = Attrs} = Packet, _JID, From, - #jid{lresource = <<"">>} = To) -> - case xml:get_attr_s(<<"type">>, Attrs) of - <<"error">> -> - case xml:get_subtag(Packet, <<"event">>) of - #xmlel{attrs = EventAttrs} -> - case xml:get_attr_s(<<"xmlns">>, EventAttrs) of - ?NS_PUBSUB_EVENT -> - ?DEBUG("Dropping PEP error message from ~s to ~s", - [jlib:jid_to_string(From), - jlib:jid_to_string(To)]), - drop; - _ -> - Packet - end; - false -> - Packet - end; - _ -> - Packet - end; -drop_pep_error(Acc, _JID, _From, _To) -> Acc. - -%% ------- %% user remove hook handling function %% @@ -1103,8 +1078,6 @@ terminate(_Reason, ?MODULE, disco_sm_features, 75), ejabberd_hooks:delete(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75), - ejabberd_hooks:delete(c2s_filter_packet_in, ServerHost, - ?MODULE, drop_pep_error, 75), gen_iq_handler:remove_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB), gen_iq_handler:remove_iq_handler(ejabberd_sm, @@ -2550,7 +2523,8 @@ subscribe_node(Host, Node, From, JID, Configuration) -> {TNode, {Result, subscribed, SubId, send_last}}} -> NodeId = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, - send_items(Host, Node, NodeId, Type, Subscriber, last), + Options = TNode#pubsub_node.options, + send_items(Host, Node, NodeId, Type, Options, Subscriber, last), case Result of default -> {result, Reply({subscribed, SubId})}; _ -> {result, Result} @@ -3018,11 +2992,12 @@ get_allowed_items_call(Host, NodeIdx, From, Type, Options, Owners, RSM) -> %% Node = pubsubNode() %% NodeId = pubsubNodeId() %% Type = pubsubNodeType() +%% Options = mod_pubsubnodeOptions() %% LJID = {U, S, []} %% Number = last | integer() %% @doc <p>Resend the items of a node to the user.</p> %% @todo use cache-last-item feature -send_items(Host, Node, NodeId, Type, LJID, last) -> +send_items(Host, Node, NodeId, Type, Options, LJID, last) -> Stanza = case get_cached_item(Host, NodeId) of undefined -> % special ODBC optimization, works only with node_hometree_odbc, node_flat_odbc and node_pep_odbc @@ -3047,8 +3022,8 @@ send_items(Host, Node, NodeId, Type, LJID, last) -> itemsEls([LastItem])}], ModifNow, ModifUSR) end, - dispatch_items(Host, LJID, Node, Stanza); -send_items(Host, Node, NodeId, Type, LJID, Number) -> + dispatch_items(Host, LJID, Node, Options, Stanza); +send_items(Host, Node, NodeId, Type, Options, LJID, Number) -> ToSend = case node_action(Host, Type, get_items, [NodeId, LJID]) of @@ -3076,20 +3051,23 @@ send_items(Host, Node, NodeId, Type, LJID, Number) -> attrs = nodeAttr(Node), children = itemsEls(ToSend)}]) end, - dispatch_items(Host, LJID, Node, Stanza). + dispatch_items(Host, LJID, Node, Options, Stanza). --spec(dispatch_items/4 :: +-spec(dispatch_items/5 :: ( - From :: mod_pubsub:host(), - To :: jid(), - Node :: mod_pubsub:nodeId(), - Stanza :: xmlel() | undefined) + From :: mod_pubsub:host(), + To :: jid(), + Node :: mod_pubsub:nodeId(), + Options :: mod_pubsub:nodeOptions(), + Stanza :: xmlel() | undefined) -> any() ). -dispatch_items(_From, _To, _Node, _Stanza = undefined) -> ok; +dispatch_items(_From, _To, _Node, _Options, _Stanza = undefined) -> ok; dispatch_items({FromU, FromS, FromR} = From, {ToU, ToS, ToR} = To, Node, - Stanza) -> + Options, BaseStanza) -> + NotificationType = get_option(Options, notification_type, headline), + Stanza = add_message_type(BaseStanza, NotificationType), C2SPid = case ejabberd_sm:get_session_pid(ToU, ToS, ToR) of ToPid when is_pid(ToPid) -> ToPid; _ -> @@ -3106,7 +3084,9 @@ dispatch_items({FromU, FromS, FromR} = From, {ToU, ToS, ToR} = To, Node, service_jid(From), jlib:make_jid(To), Stanza) end; -dispatch_items(From, To, _Node, Stanza) -> +dispatch_items(From, To, _Node, Options, BaseStanza) -> + NotificationType = get_option(Options, notification_type, headline), + Stanza = add_message_type(BaseStanza, NotificationType), ejabberd_router:route(service_jid(From), jlib:make_jid(To), Stanza). %% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response} @@ -4091,10 +4071,7 @@ broadcast_stanza(Host, _Node, _NodeId, _Type, NodeOptions, SubsByDepth, NotifyTy NotificationType = get_option(NodeOptions, notification_type, headline), BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but usefull From = service_jid(Host), - Stanza = case NotificationType of - normal -> BaseStanza; - MsgType -> add_message_type(BaseStanza, iolist_to_binary(atom_to_list(MsgType))) - end, + Stanza = add_message_type(BaseStanza, NotificationType), %% Handles explicit subscriptions SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth), lists:foreach(fun ({LJID, NodeName, SubIDs}) -> @@ -4126,10 +4103,8 @@ broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, NodeId, Type, Nod SenderResource = user_resource(LUser, LServer, LResource), case ejabberd_sm:get_session_pid(LUser, LServer, SenderResource) of C2SPid when is_pid(C2SPid) -> - Stanza = case get_option(NodeOptions, notification_type, headline) of - normal -> BaseStanza; - MsgType -> add_message_type(BaseStanza, iolist_to_binary(atom_to_list(MsgType))) - end, + NotificationType = get_option(NodeOptions, notification_type, headline), + Stanza = add_message_type(BaseStanza, NotificationType), %% set the from address on the notification to the bare JID of the account owner %% Also, add "replyto" if entity has presence subscription to the account owner %% See XEP-0163 1.1 section 4.3.1 @@ -4966,10 +4941,19 @@ itemsEls(Items) -> #xmlel{name = <<"item">>, attrs = itemAttr(ItemId), children = Payload} end, Items). +-spec(add_message_type/2 :: +( + Message :: xmlel(), + Type :: atom()) + -> xmlel() +). + +add_message_type(Message, normal) -> Message; add_message_type(#xmlel{name = <<"message">>, attrs = Attrs, children = Els}, Type) -> #xmlel{name = <<"message">>, - attrs = [{<<"type">>, Type} | Attrs], children = Els}; + attrs = [{<<"type">>, jlib:atom_to_binary(Type)} | Attrs], + children = Els}; add_message_type(XmlEl, _Type) -> XmlEl. %% Place of <headers/> changed at the bottom of the stanza diff --git a/src/mod_roster.erl b/src/mod_roster.erl index e60337cda..605e8e367 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -64,7 +64,7 @@ start(Host, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, one_queue), - case gen_mod:db_type(Opts) of + case gen_mod:db_type(Host, Opts) of mnesia -> mnesia:create_table(roster, [{disc_copies, [node()]}, diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl index 16800ded4..4c3f177ef 100644 --- a/src/mod_shared_roster.erl +++ b/src/mod_shared_roster.erl @@ -58,7 +58,7 @@ group_host = {<<"">>, <<"">>} :: {binary(), binary()}}). start(Host, Opts) -> - case gen_mod:db_type(Opts) of + case gen_mod:db_type(Host, Opts) of mnesia -> mnesia:create_table(sr_group, [{disc_copies, [node()]}, diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index 7d2860ee6..ba23d0688 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -52,7 +52,7 @@ -define(PROCNAME, ejabberd_mod_vcard). start(Host, Opts) -> - case gen_mod:db_type(Opts) of + case gen_mod:db_type(Host, Opts) of mnesia -> mnesia:create_table(vcard, [{disc_only_copies, [node()]}, diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl index 97d9abbb4..41a07bbc3 100644 --- a/src/mod_vcard_xupdate.erl +++ b/src/mod_vcard_xupdate.erl @@ -28,7 +28,7 @@ %%==================================================================== start(Host, Opts) -> - case gen_mod:db_type(Opts) of + case gen_mod:db_type(Host, Opts) of mnesia -> mnesia:create_table(vcard_xupdate, [{disc_copies, [node()]}, diff --git a/src/odbc_queries.erl b/src/odbc_queries.erl index 1fa16b896..7dee1a047 100644 --- a/src/odbc_queries.erl +++ b/src/odbc_queries.erl @@ -27,9 +27,11 @@ -author("mremond@process-one.net"). --export([get_db_type/0, update_t/4, sql_transaction/2, - get_last/2, set_last_t/4, del_last/2, get_password/2, - set_password_t/3, add_user/3, del_user/2, +-export([get_db_type/0, update/5, update_t/4, sql_transaction/2, + get_last/2, set_last_t/4, del_last/2, + get_password/2, get_password_scram/2, + set_password_t/3, set_password_scram_t/6, + add_user/3, add_user_scram/6, del_user/2, del_user_return_password/3, list_users/1, list_users/2, users_number/1, users_number/2, add_spool_sql/2, add_spool/2, get_and_del_spool_msg_t/2, del_spool_msg/2, @@ -157,6 +159,12 @@ get_password(LServer, Username) -> [<<"select password from users where username='">>, Username, <<"';">>]). +get_password_scram(LServer, Username) -> + ejabberd_odbc:sql_query( + LServer, + [<<"select password, serverkey, salt, iterationcount from users where " + "username='">>, Username, <<"';">>]). + set_password_t(LServer, Username, Pass) -> ejabberd_odbc:sql_transaction(LServer, fun () -> @@ -168,12 +176,39 @@ set_password_t(LServer, Username, Pass) -> <<"'">>]) end). +set_password_scram_t(LServer, Username, + StoredKey, ServerKey, Salt, IterationCount) -> + ejabberd_odbc:sql_transaction(LServer, + fun () -> + update_t(<<"users">>, + [<<"username">>, + <<"password">>, + <<"serverkey">>, + <<"salt">>, + <<"iterationcount">>], + [Username, StoredKey, + ServerKey, Salt, + IterationCount], + [<<"username='">>, Username, + <<"'">>]) + end). + add_user(LServer, Username, Pass) -> ejabberd_odbc:sql_query(LServer, [<<"insert into users(username, password) " "values ('">>, Username, <<"', '">>, Pass, <<"');">>]). +add_user_scram(LServer, Username, + StoredKey, ServerKey, Salt, IterationCount) -> + ejabberd_odbc:sql_query(LServer, + [<<"insert into users(username, password, serverkey, salt, iterationcount) " + "values ('">>, + Username, <<"', '">>, StoredKey, <<"', '">>, + ServerKey, <<"', '">>, + Salt, <<"', '">>, + IterationCount, <<"');">>]). + del_user(LServer, Username) -> ejabberd_odbc:sql_query(LServer, [<<"delete from users where username='">>, Username, @@ -229,6 +264,7 @@ users_number(LServer) -> Type = ejabberd_config:get_option({odbc_type, LServer}, fun(pgsql) -> pgsql; (mysql) -> mysql; + (sqlite) -> sqlite; (odbc) -> odbc end, odbc), case Type of diff --git a/src/scram.erl b/src/scram.erl index c8c85e8d5..8c7935210 100644 --- a/src/scram.erl +++ b/src/scram.erl @@ -41,7 +41,7 @@ salted_password(Password, Salt, IterationCount) -> -spec client_key(binary()) -> binary(). client_key(SaltedPassword) -> - crypto:sha_mac(SaltedPassword, <<"Client Key">>). + sha_mac(SaltedPassword, <<"Client Key">>). -spec stored_key(binary()) -> binary(). @@ -50,12 +50,12 @@ stored_key(ClientKey) -> p1_sha:sha1(ClientKey). -spec server_key(binary()) -> binary(). server_key(SaltedPassword) -> - crypto:sha_mac(SaltedPassword, <<"Server Key">>). + sha_mac(SaltedPassword, <<"Server Key">>). -spec client_signature(binary(), binary()) -> binary(). client_signature(StoredKey, AuthMessage) -> - crypto:sha_mac(StoredKey, AuthMessage). + sha_mac(StoredKey, AuthMessage). -spec client_key(binary(), binary()) -> binary(). @@ -67,20 +67,25 @@ client_key(ClientProof, ClientSignature) -> -spec server_signature(binary(), binary()) -> binary(). server_signature(ServerKey, AuthMessage) -> - crypto:sha_mac(ServerKey, AuthMessage). + sha_mac(ServerKey, AuthMessage). hi(Password, Salt, IterationCount) -> - U1 = crypto:sha_mac(Password, <<Salt/binary, 0, 0, 0, 1>>), + U1 = sha_mac(Password, <<Salt/binary, 0, 0, 0, 1>>), list_to_binary(lists:zipwith(fun (X, Y) -> X bxor Y end, binary_to_list(U1), binary_to_list(hi_round(Password, U1, IterationCount - 1)))). hi_round(Password, UPrev, 1) -> - crypto:sha_mac(Password, UPrev); + sha_mac(Password, UPrev); hi_round(Password, UPrev, IterationCount) -> - U = crypto:sha_mac(Password, UPrev), + U = sha_mac(Password, UPrev), list_to_binary(lists:zipwith(fun (X, Y) -> X bxor Y end, binary_to_list(U), binary_to_list(hi_round(Password, U, IterationCount - 1)))). + +sha_mac(Key, Data) -> + Context1 = crypto:hmac_init(sha, Key), + Context2 = crypto:hmac_update(Context1, Data), + crypto:hmac_final(Context2). diff --git a/src/translate.erl b/src/translate.erl index 9e48e0b7a..277dfa445 100644 --- a/src/translate.erl +++ b/src/translate.erl @@ -81,7 +81,7 @@ load_file(Lang, File) -> io:setopts(Fd, [{encoding,latin1}]), load_file_loop(Fd, 1, File, Lang), file:close(Fd); - Error -> + {error, Error} -> ExitText = iolist_to_binary([File, ": ", file:format_error(Error)]), ?ERROR_MSG("Problem loading translation file ~n~s", |