aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ejabberd_app.erl4
-rw-r--r--src/ejabberd_auth.erl10
-rw-r--r--src/ejabberd_auth_odbc.erl425
-rw-r--r--src/ejabberd_auth_riak.erl7
-rw-r--r--src/ejabberd_c2s.erl78
-rw-r--r--src/ejabberd_commands.erl8
-rw-r--r--src/ejabberd_config.erl109
-rw-r--r--src/ejabberd_hooks.erl169
-rw-r--r--src/ejabberd_http.erl322
-rw-r--r--src/ejabberd_http_poll.erl425
-rw-r--r--src/ejabberd_http_ws.erl355
-rw-r--r--src/ejabberd_listener.erl11
-rw-r--r--src/ejabberd_logger.erl4
-rw-r--r--src/ejabberd_odbc.erl80
-rw-r--r--src/ejabberd_odbc_sup.erl90
-rw-r--r--src/ejabberd_rdbms.erl2
-rw-r--r--src/ejabberd_receiver.erl8
-rw-r--r--src/ejabberd_riak.erl7
-rw-r--r--src/ejabberd_riak_sup.erl9
-rw-r--r--src/ejabberd_router_multicast.erl215
-rw-r--r--src/ejabberd_s2s_in.erl2
-rw-r--r--src/ejabberd_s2s_out.erl2
-rw-r--r--src/ejabberd_sm.erl301
-rw-r--r--src/ejabberd_sm_mnesia.erl145
-rw-r--r--src/ejabberd_sm_odbc.erl169
-rw-r--r--src/ejabberd_sm_redis.erl209
-rw-r--r--src/ejabberd_socket.erl9
-rw-r--r--src/ejabberd_sup.erl21
-rw-r--r--src/ejabberd_update.erl7
-rw-r--r--src/ejabberd_web_admin.erl34
-rw-r--r--src/ejabberd_websocket.erl404
-rw-r--r--src/ejabberd_xmlrpc.erl2
-rw-r--r--src/eldap.erl4
-rw-r--r--src/ext_mod.erl486
-rw-r--r--src/gen_mod.erl34
-rw-r--r--src/jlib.erl3
-rw-r--r--src/mod_admin_extra.erl1583
-rw-r--r--src/mod_announce.erl2
-rw-r--r--src/mod_blocking.erl124
-rw-r--r--src/mod_caps.erl9
-rw-r--r--src/mod_carboncopy.erl19
-rw-r--r--src/mod_irc.erl2
-rw-r--r--src/mod_irc_connection.erl10
-rw-r--r--src/mod_last.erl2
-rw-r--r--src/mod_muc.erl2
-rw-r--r--src/mod_muc_admin.erl888
-rw-r--r--src/mod_muc_log.erl8
-rw-r--r--src/mod_muc_room.erl241
-rw-r--r--src/mod_multicast.erl1162
-rw-r--r--src/mod_offline.erl40
-rw-r--r--src/mod_ping.erl2
-rw-r--r--src/mod_privacy.erl80
-rw-r--r--src/mod_private.erl2
-rw-r--r--src/mod_pubsub.erl95
-rw-r--r--src/mod_pubsub_odbc.erl98
-rw-r--r--src/mod_roster.erl2
-rw-r--r--src/mod_shared_roster.erl2
-rw-r--r--src/mod_vcard.erl2
-rw-r--r--src/mod_vcard_xupdate.erl2
-rw-r--r--src/odbc_queries.erl42
-rw-r--r--src/scram.erl19
-rw-r--r--src/translate.erl2
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",