aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.in7
-rwxr-xr-xsrc/configure34
-rw-r--r--src/configure.ac9
-rw-r--r--src/ejabberd.app2
-rw-r--r--src/ejabberd.hrl7
-rw-r--r--src/ejabberd_admin.erl110
-rw-r--r--src/ejabberd_app.erl4
-rw-r--r--src/ejabberd_auth_anonymous.erl38
-rw-r--r--src/ejabberd_c2s.erl1650
-rw-r--r--src/ejabberd_c2s.hrl88
-rw-r--r--src/ejabberd_captcha.erl188
-rw-r--r--src/ejabberd_cluster.erl252
-rw-r--r--src/ejabberd_config.erl13
-rw-r--r--src/ejabberd_frontend_socket.erl87
-rw-r--r--src/ejabberd_listener.erl46
-rw-r--r--src/ejabberd_local.erl50
-rw-r--r--src/ejabberd_node_groups.erl43
-rw-r--r--src/ejabberd_receiver.erl135
-rw-r--r--src/ejabberd_router.erl52
-rw-r--r--src/ejabberd_s2s.erl93
-rw-r--r--src/ejabberd_s2s_out.erl19
-rw-r--r--src/ejabberd_sm.erl387
-rw-r--r--src/ejabberd_socket.erl64
-rw-r--r--src/ejabberd_sup.erl16
-rw-r--r--src/ejabberd_xmlrpc.erl467
-rw-r--r--src/ejabberdctl.template30
-rw-r--r--src/etop_defs.hrl29
-rw-r--r--src/etop_tr.erl130
-rw-r--r--src/expat_erl.c29
-rw-r--r--src/floodcheck.erl205
-rw-r--r--src/http_p1.erl337
-rw-r--r--src/jlib.hrl115
-rw-r--r--src/licence.hrl1
-rw-r--r--src/log.hrl38
-rw-r--r--src/mochiglobal.erl107
-rw-r--r--src/mod_ack.erl420
-rw-r--r--src/mod_admin_p1.erl1347
-rw-r--r--src/mod_antiflood.erl186
-rw-r--r--src/mod_autofilter.erl129
-rw-r--r--src/mod_c2s_debug.erl205
-rw-r--r--src/mod_caps.erl20
-rw-r--r--src/mod_filter.erl370
-rw-r--r--src/mod_ip_blacklist.erl2
-rw-r--r--src/mod_mnesia_mngt.erl147
-rw-r--r--src/mod_muc/mod_muc.erl423
-rw-r--r--src/mod_muc/mod_muc_log.erl11
-rw-r--r--src/mod_muc/mod_muc_room.erl294
-rw-r--r--src/mod_muc/mod_muc_room.hrl1
-rw-r--r--src/mod_offline.erl102
-rw-r--r--src/mod_ping.erl8
-rw-r--r--src/mod_privacy.hrl2
-rw-r--r--src/mod_proxy65/mod_proxy65_sm.erl10
-rw-r--r--src/mod_pubsub/mod_pubsub.erl545
-rw-r--r--src/mod_pubsub/mod_pubsub_odbc.erl539
-rw-r--r--src/mod_pubsub/node_flat_odbc.erl3
-rw-r--r--src/mod_pubsub/node_hometree.erl12
-rw-r--r--src/mod_pubsub/node_pep_odbc.erl1
-rw-r--r--src/mod_pubsub/pubsub.hrl2
-rw-r--r--src/mod_pubsub/pubsub_clean.erl42
-rw-r--r--src/mod_pubsub/pubsub_debug.erl113
-rw-r--r--src/mod_pubsub/pubsub_odbc.patch260
-rw-r--r--src/mod_pubsub/pubsub_subscription.erl9
-rw-r--r--src/mod_service_log.erl9
-rw-r--r--src/mod_shared_roster.erl2
-rw-r--r--src/mod_support.erl260
-rw-r--r--src/mod_xmlrpc.erl1066
-rw-r--r--src/odbc/ejabberd_odbc.erl21
-rw-r--r--src/odbc/mysql.sql13
-rw-r--r--src/odbc/odbc_queries.erl20
-rw-r--r--src/p1_fsm.erl138
-rw-r--r--src/sha.erl7
-rw-r--r--src/tcp_serv.erl156
-rw-r--r--src/tls/tls_drv.c4
-rw-r--r--src/web/bosh.hrl34
-rw-r--r--src/web/ejabberd_bosh.erl994
-rw-r--r--src/web/ejabberd_http.erl171
-rw-r--r--src/web/ejabberd_http.hrl20
-rw-r--r--src/web/ejabberd_http_bind.erl192
-rw-r--r--src/web/ejabberd_http_bindjson.erl1294
-rw-r--r--src/web/ejabberd_http_poll.erl57
-rw-r--r--src/web/ejabberd_http_ws.erl214
-rw-r--r--src/web/ejabberd_http_wsjson.erl219
-rw-r--r--src/web/ejabberd_websocket.erl435
-rw-r--r--src/web/ejabberd_ws.erl80
-rw-r--r--src/web/mochijson2.erl782
-rw-r--r--src/web/mod_bosh.erl212
-rw-r--r--src/web/mod_http_bind.erl10
-rw-r--r--src/web/mod_http_bindjson.erl156
-rw-r--r--src/web/mod_http_fileserver.erl428
-rw-r--r--src/web/mod_http_fileserver_log.erl167
-rw-r--r--src/web/pshb_http.erl416
-rw-r--r--src/web/simple_ws_check.erl11
-rw-r--r--src/web/websocket_test.erl19
-rw-r--r--src/web/xmpp_json.erl362
-rw-r--r--src/xml.erl26
-rw-r--r--src/xml_stream.erl9
-rw-r--r--src/xmlrpc.erl187
-rw-r--r--src/xmlrpc_decode.erl218
-rw-r--r--src/xmlrpc_encode.erl145
-rw-r--r--src/xmlrpc_http.erl210
-rw-r--r--src/xmlrpc_util.erl37
101 files changed, 16866 insertions, 2020 deletions
diff --git a/src/Makefile.in b/src/Makefile.in
index 42af5b2f2..12da612a2 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -74,6 +74,11 @@ ifeq (@pam@, pam)
INSTALL_EPAM=install -m 750 $(O_USER) epam $(PBINDIR)
endif
+ifeq (@flash_hack@, true)
+ ERLC_FLAGS+=-DENABLE_FLASH_HACK
+ CPPFLAGS+=-DENABLE_FLASH_HACK
+endif
+
prefix = @prefix@
exec_prefix = @exec_prefix@
@@ -168,7 +173,7 @@ mostlyclean-recursive maintainer-clean-recursive:
@ERLC@ -W $(EFLAGS) $*.erl
$(ERLSHLIBS): %.so: %.c
- $(CC) $(CFLAGS) $(LDFLAGS) $(LIBS) \
+ $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(LIBS) \
$(subst ../,,$(subst .so,.c,$@)) \
$(EXPAT_LIBS) \
$(EXPAT_CFLAGS) \
diff --git a/src/configure b/src/configure
index 89e32a5f1..e0931198c 100755
--- a/src/configure
+++ b/src/configure
@@ -1,6 +1,6 @@
#! /bin/sh
# Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.68 for ejabberd 2.1.x.
+# Generated by GNU Autoconf 2.68 for ejabberd 2.2.10.
#
# Report bugs to <ejabberd@process-one.net>.
#
@@ -560,8 +560,8 @@ MAKEFLAGS=
# Identity of this package.
PACKAGE_NAME='ejabberd'
PACKAGE_TARNAME='ejabberd'
-PACKAGE_VERSION='2.1.x'
-PACKAGE_STRING='ejabberd 2.1.x'
+PACKAGE_VERSION='2.2.10'
+PACKAGE_STRING='ejabberd 2.2.10'
PACKAGE_BUGREPORT='ejabberd@process-one.net'
PACKAGE_URL=''
@@ -624,6 +624,7 @@ nif
full_xml
transient_supervisors
db_type
+flash_hack
roster_gateway_workaround
hipe
PAM_LIBS
@@ -728,6 +729,7 @@ enable_pam
with_pam
enable_hipe
enable_roster_gateway_workaround
+enable_flash_hack
enable_mssql
enable_transient_supervisors
enable_full_xml
@@ -1288,7 +1290,7 @@ if test "$ac_init_help" = "long"; then
# Omit some internal or obsolete options to make the list less imposing.
# This message is too long to be a string in the A/UX 3.1 sh.
cat <<_ACEOF
-\`configure' configures ejabberd 2.1.x to adapt to many kinds of systems.
+\`configure' configures ejabberd 2.2.10 to adapt to many kinds of systems.
Usage: $0 [OPTION]... [VAR=VALUE]...
@@ -1354,7 +1356,7 @@ fi
if test -n "$ac_init_help"; then
case $ac_init_help in
- short | recursive ) echo "Configuration of ejabberd 2.1.x:";;
+ short | recursive ) echo "Configuration of ejabberd 2.2.10:";;
esac
cat <<\_ACEOF
@@ -1377,6 +1379,7 @@ Optional Features:
--enable-roster-gateway-workaround
turn on workaround for processing gateway
subscriptions (default: no)
+ --enable-flash-hack support Adobe Flash client XML (default: no)
--enable-mssql use Microsoft SQL Server database (default: no,
requires --enable-odbc)
--enable-transient_supervisors
@@ -1479,7 +1482,7 @@ fi
test -n "$ac_init_help" && exit $ac_status
if $ac_init_version; then
cat <<\_ACEOF
-ejabberd configure 2.1.x
+ejabberd configure 2.2.10
generated by GNU Autoconf 2.68
Copyright (C) 2010 Free Software Foundation, Inc.
@@ -1823,7 +1826,7 @@ cat >config.log <<_ACEOF
This file contains any messages produced by compilers while
running configure, to aid debugging if configure makes a mistake.
-It was created by ejabberd $as_me 2.1.x, which was
+It was created by ejabberd $as_me 2.2.10, which was
generated by GNU Autoconf 2.68. Invocation command line was
$ $0 $@
@@ -4644,6 +4647,19 @@ fi
+# Check whether --enable-flash_hack was given.
+if test "${enable_flash_hack+set}" = set; then :
+ enableval=$enable_flash_hack; case "${enableval}" in
+ yes) flash_hack=true ;;
+ no) flash_hack=false ;;
+ *) as_fn_error $? "bad value ${enableval} for --enable-flash-hack" "$LINENO" 5 ;;
+esac
+else
+ flash_hack=false
+fi
+
+
+
# Check whether --enable-mssql was given.
if test "${enable_mssql+set}" = set; then :
enableval=$enable_mssql; case "${enableval}" in
@@ -5690,7 +5706,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
# report actual input values of CONFIG_FILES etc. instead of their
# values after options handling.
ac_log="
-This file was extended by ejabberd $as_me 2.1.x, which was
+This file was extended by ejabberd $as_me 2.2.10, which was
generated by GNU Autoconf 2.68. Invocation command line was
CONFIG_FILES = $CONFIG_FILES
@@ -5743,7 +5759,7 @@ _ACEOF
cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
ac_cs_version="\\
-ejabberd config.status 2.1.x
+ejabberd config.status 2.2.10
configured by $0, generated by GNU Autoconf 2.68,
with options \\"\$ac_cs_config\\"
diff --git a/src/configure.ac b/src/configure.ac
index 1d25dd871..5492f429a 100644
--- a/src/configure.ac
+++ b/src/configure.ac
@@ -67,6 +67,15 @@ AC_ARG_ENABLE(roster_gateway_workaround,
esac],[roster_gateway_workaround=false])
AC_SUBST(roster_gateway_workaround)
+AC_ARG_ENABLE(flash_hack,
+[AC_HELP_STRING([--enable-flash-hack], [support Adobe Flash client XML (default: no)])],
+[case "${enableval}" in
+ yes) flash_hack=true ;;
+ no) flash_hack=false ;;
+ *) AC_MSG_ERROR(bad value ${enableval} for --enable-flash-hack) ;;
+esac],[flash_hack=false])
+AC_SUBST(flash_hack)
+
AC_ARG_ENABLE(mssql,
[AC_HELP_STRING([--enable-mssql], [use Microsoft SQL Server database (default: no, requires --enable-odbc)])],
[case "${enableval}" in
diff --git a/src/ejabberd.app b/src/ejabberd.app
index 1af67fbae..193f7e399 100644
--- a/src/ejabberd.app
+++ b/src/ejabberd.app
@@ -2,7 +2,7 @@
{application, ejabberd,
[{description, "ejabberd"},
- {vsn, "2.1.11"},
+ {vsn, "2.2.11"},
{modules, [acl,
adhoc,
configure,
diff --git a/src/ejabberd.hrl b/src/ejabberd.hrl
index 7d1f5546d..044ba46c9 100644
--- a/src/ejabberd.hrl
+++ b/src/ejabberd.hrl
@@ -31,6 +31,13 @@
-define(CONFIG_PATH, "ejabberd.cfg").
-define(LOG_PATH, "ejabberd.log").
+-ifdef(ENABLE_FLASH_HACK).
+-define(FLASH_HACK, true).
+-else.
+-define(FLASH_HACK, false).
+-endif.
+
+
-define(EJABBERD_URI, "http://www.process-one.net/en/ejabberd/").
-define(S2STIMEOUT, 600000).
diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl
index 40c8b8dca..294697f51 100644
--- a/src/ejabberd_admin.erl
+++ b/src/ejabberd_admin.erl
@@ -30,6 +30,7 @@
-export([start/0, stop/0,
%% Server
status/0, reopen_log/0,
+ stop_migrate/1, migrate/1,
stop_kindly/2, send_service_message_all_mucs/2,
%% Erlang
update_list/0, update/1,
@@ -47,7 +48,9 @@
install_fallback_mnesia/1,
dump_to_textfile/1, dump_to_textfile/2,
mnesia_change_nodename/4,
- restore/1 % Still used by some modules
+ restore/1, % Still used by some modules
+ moderate_room_history/2,
+ persist_recent_messages/0
]).
-include("ejabberd.hrl").
@@ -92,6 +95,17 @@ commands() ->
module = ?MODULE, function = stop_kindly,
args = [{delay, integer}, {announcement, string}],
result = {res, rescode}},
+ #ejabberd_commands{name = migrate, tags = [server],
+ desc = "Try to migrate C2S/BOSH/MUC sessions to other nodes",
+ module = ?MODULE, function = migrate,
+ args = [{delay, integer}],
+ result = {res, rescode}},
+ #ejabberd_commands{name = stop_migrate, tags = [server],
+ desc = "Try to migrate C2S/BOSH/MUC sessions to other"
+ "nodes and then stop",
+ module = ?MODULE, function = stop_migrate,
+ args = [{delay, integer}],
+ result = {res, rescode}},
#ejabberd_commands{name = get_loglevel, tags = [logs, server],
desc = "Get the current loglevel",
module = ejabberd_loglevel, function = get,
@@ -200,9 +214,40 @@ commands() ->
#ejabberd_commands{name = install_fallback, tags = [mnesia],
desc = "Install the database from a fallback file",
module = ?MODULE, function = install_fallback_mnesia,
- args = [{file, string}], result = {res, restuple}}
+ args = [{file, string}], result = {res, restuple}},
+ #ejabberd_commands{name = moderate_room_history, tags = [server],
+ desc = "Clean messages from the short-term MUC storage",
+ module = ?MODULE, function = moderate_room_history,
+ args = [{room, string}, {nick, string}],
+ result = {res, restuple}},
+ #ejabberd_commands{name = persist_recent_messages, tags = [server],
+ desc = "Force recent muc messages to be savd on DB",
+ module = ?MODULE, function = persist_recent_messages,
+ args = [],
+ result = {res, restuple}}
].
+%%%
+%%% MUC moderation
+%%%
+%%% Same room can be replicated into different nodes,
+%%% call all of them.
+moderate_room_history(Room, Nick) ->
+ {Res, BadNodes} = rpc:multicall(mod_muc, moderate_room_history, [Room, Nick], 5000),
+ B = case BadNodes of
+ [] ->
+ "";
+ _ ->
+ io_lib:format("Bad nodes: ~p", [BadNodes])
+ end,
+ {ok, io_lib:format("Deleted: ~p ~s", [Res, B])}.
+
+persist_recent_messages() ->
+ Saved = [ {Host, mod_muc:persist_recent_messages(Host)} || Host <- ?MYHOSTS],
+ R = lists:map(fun({Host, {RoomsPersisted, Messages}}) ->
+ io_lib:format("Host '~s' , ~p messages persisted in ~p rooms\n", [Host, Messages, RoomsPersisted])
+ end, Saved),
+ {ok,io_lib:format("~s", [R])}.
%%%
%%% Server management
@@ -294,6 +339,67 @@ send_service_message_all_mucs(Subject, AnnouncementText) ->
?MYHOSTS).
%%%
+%%% Migrate w/o stopping
+%%%
+migrate(DelaySeconds) ->
+ WaitingDesc = io_lib:format("Starting migration, this will take ~p seconds",
+ [DelaySeconds]),
+ Steps = [
+ {"Stopping ejabberd port listeners",
+ ejabberd_listener, stop_listeners, []},
+ {WaitingDesc, ejabberd_cluster, shutdown_migrate,
+ [DelaySeconds * 1000]}
+ ],
+ NumberLast = length(Steps),
+ TimestampStart = calendar:datetime_to_gregorian_seconds({date(), time()}),
+ lists:foldl(
+ fun({Desc, Mod, Func, Args}, NumberThis) ->
+ SecondsDiff =
+ calendar:datetime_to_gregorian_seconds({date(), time()})
+ - TimestampStart,
+ io:format("[~p/~p ~ps] ~s... ",
+ [NumberThis, NumberLast, SecondsDiff, Desc]),
+ Result = apply(Mod, Func, Args),
+ io:format("~p~n", [Result]),
+ NumberThis+1
+ end,
+ 1,
+ Steps),
+ ok.
+
+%%%
+%%% Migrate and stop
+%%%
+stop_migrate(DelaySeconds) ->
+ WaitingDesc = io_lib:format("Starting migration, this will take ~p seconds",
+ [DelaySeconds]),
+ Steps = [
+ {"Stopping ejabberd port listeners",
+ ejabberd_listener, stop_listeners, []},
+ {WaitingDesc, ejabberd_cluster, shutdown_migrate,
+ [DelaySeconds * 1000]},
+ {"Stopping ejabberd", application, stop, [ejabberd]},
+ {"Stopping Mnesia", mnesia, stop, []},
+ {"Stopping Erlang node", init, stop, []}
+ ],
+ NumberLast = length(Steps),
+ TimestampStart = calendar:datetime_to_gregorian_seconds({date(), time()}),
+ lists:foldl(
+ fun({Desc, Mod, Func, Args}, NumberThis) ->
+ SecondsDiff =
+ calendar:datetime_to_gregorian_seconds({date(), time()})
+ - TimestampStart,
+ io:format("[~p/~p ~ps] ~s... ",
+ [NumberThis, NumberLast, SecondsDiff, Desc]),
+ Result = apply(Mod, Func, Args),
+ io:format("~p~n", [Result]),
+ NumberThis+1
+ end,
+ 1,
+ Steps),
+ ok.
+
+%%%
%%% ejabberd_update
%%%
diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl
index ea467cbb8..8db63189e 100644
--- a/src/ejabberd_app.erl
+++ b/src/ejabberd_app.erl
@@ -67,7 +67,10 @@ start(normal, _Args) ->
%ejabberd_debug:eprof_start(),
%ejabberd_debug:fprof_start(),
maybe_add_nameservers(),
+ {ok, Pid} = ejabberd_cluster:start(),
start_modules(),
+ ejabberd_cluster:announce(Pid),
+ ejabberd_node_groups:start(),
ejabberd_listener:start_listeners(),
?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]),
Sup;
@@ -78,6 +81,7 @@ start(_, _) ->
%% This function is called when an application is about to be stopped,
%% before shutting down the processes of the application.
prep_stop(State) ->
+ ejabberd_cluster:shutdown(),
stop_modules(),
ejabberd_admin:stop(),
broadcast_c2s_shutdown(),
diff --git a/src/ejabberd_auth_anonymous.erl b/src/ejabberd_auth_anonymous.erl
index 57a33746d..b314e51af 100644
--- a/src/ejabberd_auth_anonymous.erl
+++ b/src/ejabberd_auth_anonymous.erl
@@ -34,6 +34,7 @@
anonymous_user_exist/2,
allow_multiple_connections/1,
register_connection/3,
+ unregister_migrated_connection/3,
unregister_connection/3
]).
@@ -62,12 +63,16 @@
%% Register to login / logout events
start(Host) ->
%% TODO: Check cluster mode
+ update_tables(),
mnesia:create_table(anonymous, [{ram_copies, [node()]},
- {type, bag},
+ {type, bag}, {local_content, true},
{attributes, record_info(fields, anonymous)}]),
+ mnesia:add_table_copy(anonymous, node(), ram_copies),
%% The hooks are needed to add / remove users from the anonymous tables
ejabberd_hooks:add(sm_register_connection_hook, Host,
?MODULE, register_connection, 100),
+ ejabberd_hooks:add(sm_remove_migrated_connection_hook, Host,
+ ?MODULE, unregister_migrated_connection, 100),
ejabberd_hooks:add(sm_remove_connection_hook, Host,
?MODULE, unregister_connection, 100),
ok.
@@ -124,11 +129,18 @@ anonymous_user_exist(User, Server) ->
LUser = jlib:nodeprep(User),
LServer = jlib:nameprep(Server),
US = {LUser, LServer},
- case catch mnesia:dirty_read({anonymous, US}) of
- [] ->
- false;
+ Ss = case ejabberd_cluster:get_node(US) of
+ Node when Node == node() ->
+ catch mnesia:dirty_read({anonymous, US});
+ Node ->
+ catch rpc:call(Node, mnesia, dirty_read,
+ [{anonymous, US}], 5000)
+ end,
+ case Ss of
[_H|_T] ->
- true
+ true;
+ _ ->
+ false
end.
%% Remove connection from Mnesia tables
@@ -137,7 +149,7 @@ remove_connection(SID, LUser, LServer) ->
F = fun() ->
mnesia:delete_object({anonymous, US, SID})
end,
- mnesia:transaction(F).
+ mnesia:async_dirty(F).
%% Register connection
register_connection(SID, #jid{luser = LUser, lserver = LServer}, Info) ->
@@ -146,7 +158,7 @@ register_connection(SID, #jid{luser = LUser, lserver = LServer}, Info) ->
true ->
ejabberd_hooks:run(register_user, LServer, [LUser, LServer]),
US = {LUser, LServer},
- mnesia:sync_dirty(
+ mnesia:async_dirty(
fun() -> mnesia:write(#anonymous{us = US, sid=SID})
end);
false ->
@@ -159,6 +171,10 @@ unregister_connection(SID, #jid{luser = LUser, lserver = LServer}, _) ->
LUser, LServer),
remove_connection(SID, LUser, LServer).
+%% Remove an anonymous user from the anonymous users table
+unregister_migrated_connection(SID, #jid{luser = LUser, lserver = LServer}, _) ->
+ remove_connection(SID, LUser, LServer).
+
%% Launch the hook to purge user data only for anonymous users
purge_hook(false, _LUser, _LServer) ->
ok;
@@ -249,5 +265,13 @@ remove_user(_User, _Server, _Password) ->
plain_password_required() ->
false.
+update_tables() ->
+ case catch mnesia:table_info(anonymous, local_content) of
+ false ->
+ mnesia:delete_table(anonymous);
+ _ ->
+ ok
+ end.
+
store_type() ->
plain.
diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl
index 552aa6dbb..f92898d99 100644
--- a/src/ejabberd_c2s.erl
+++ b/src/ejabberd_c2s.erl
@@ -35,7 +35,7 @@
%% External exports
-export([start/2,
stop/1,
- start_link/2,
+ start_link/3,
send_text/2,
send_element/2,
socket_type/0,
@@ -47,6 +47,9 @@
broadcast/4,
get_subscribed/1]).
+%% API:
+-export([add_rosteritem/3, del_rosteritem/2]).
+
%% gen_fsm callbacks
-export([init/1,
wait_for_stream/2,
@@ -61,48 +64,15 @@
code_change/4,
handle_info/3,
terminate/3,
- print_state/1
- ]).
+ print_state/1,
+ migrate/3,
+ migrate_shutdown/3
+ ]).
-include("ejabberd.hrl").
-include("jlib.hrl").
-include("mod_privacy.hrl").
-
--define(SETS, gb_sets).
--define(DICT, dict).
-
-%% pres_a contains all the presence available send (either through roster mechanism or directed).
-%% Directed presence unavailable remove user from pres_a.
--record(state, {socket,
- sockmod,
- socket_monitor,
- xml_socket,
- streamid,
- sasl_state,
- access,
- shaper,
- zlib = false,
- tls = false,
- tls_required = false,
- tls_enabled = false,
- tls_options = [],
- authenticated = false,
- jid,
- user = "", server = ?MYNAME, resource = "",
- sid,
- pres_t = ?SETS:new(),
- pres_f = ?SETS:new(),
- pres_a = ?SETS:new(),
- pres_i = ?SETS:new(),
- pres_last, pres_pri,
- pres_timestamp,
- pres_invis = false,
- privacy_list = #userlist{},
- conn = unknown,
- auth_module = unknown,
- ip,
- aux_fields = [],
- lang}).
+-include("ejabberd_c2s.hrl").
%-define(DBGFSM, true).
@@ -114,11 +84,12 @@
%% Module start with or without supervisor:
-ifdef(NO_TRANSIENT_SUPERVISORS).
--define(SUPERVISOR_START, ?GEN_FSM:start(ejabberd_c2s, [SockData, Opts],
- fsm_limit_opts(Opts) ++ ?FSMOPTS)).
+-define(SUPERVISOR_START, ?GEN_FSM:start(ejabberd_c2s,
+ [SockData, Opts, FSMLimitOpts],
+ FSMLimitOpts ++ ?FSMOPTS)).
-else.
-define(SUPERVISOR_START, supervisor:start_child(ejabberd_c2s_sup,
- [SockData, Opts])).
+ [SockData, Opts, FSMLimitOpts])).
-endif.
%% This is the timeout to apply between event when starting a new
@@ -133,6 +104,13 @@
"id='~s' from='~s'~s~s>"
).
+-define(FLASH_STREAM_HEADER,
+ "<?xml version='1.0'?>"
+ "<flash:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "id='~s' from='~s'~s~s>"
+ ).
+
-define(STREAM_TRAILER, "</stream:stream>").
-define(INVALID_NS_ERR, ?SERR_INVALID_NAMESPACE).
@@ -142,16 +120,30 @@
?SERRT_POLICY_VIOLATION(Lang, Text)).
-define(INVALID_FROM, ?SERR_INVALID_FROM).
+-define(NS_P1_REBIND, "p1:rebind").
+-define(NS_P1_PUSH, "p1:push").
+-define(NS_P1_ACK, "p1:ack").
+-define(NS_P1_PUSHED, "p1:pushed").
+-define(NS_P1_ATTACHMENT, "http://process-one.net/attachement").
+
+-define(C2S_P1_ACK_TIMEOUT, 10000).
+-define(MAX_OOR_TIMEOUT, 1440). %% Max allowed session duration 24h (24*60)
+-define(MAX_OOR_MESSAGES, 1000).
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
+start(StateName, #state{fsm_limit_opts = Opts} = State) ->
+ start(StateName, State, Opts);
start(SockData, Opts) ->
+ start(SockData, Opts, fsm_limit_opts(Opts)).
+
+start(SockData, Opts, FSMLimitOpts) ->
?SUPERVISOR_START.
-start_link(SockData, Opts) ->
- ?GEN_FSM:start_link(ejabberd_c2s, [SockData, Opts],
- fsm_limit_opts(Opts) ++ ?FSMOPTS).
+start_link(SockData, Opts, FSMLimitOpts) ->
+ ?GEN_FSM:start_link(ejabberd_c2s, [SockData, Opts, FSMLimitOpts],
+ FSMLimitOpts ++ ?FSMOPTS).
socket_type() ->
xml_stream.
@@ -160,6 +152,12 @@ socket_type() ->
get_presence(FsmRef) ->
?GEN_FSM:sync_send_all_state_event(FsmRef, {get_presence}, 1000).
+add_rosteritem(FsmRef, IJID, ISubscription) ->
+ ?GEN_FSM:send_all_state_event(FsmRef, {add_rosteritem, IJID, ISubscription}).
+
+del_rosteritem(FsmRef, IJID) ->
+ ?GEN_FSM:send_all_state_event(FsmRef, {del_rosteritem, IJID}).
+
get_aux_field(Key, #state{aux_fields = Opts}) ->
case lists:keysearch(Key, 1, Opts) of
{value, {_, Val}} ->
@@ -196,6 +194,12 @@ broadcast(FsmRef, Type, From, Packet) ->
stop(FsmRef) ->
?GEN_FSM:send_event(FsmRef, closed).
+migrate(FsmRef, Node, After) ->
+ erlang:send_after(After, FsmRef, {migrate, Node}).
+
+migrate_shutdown(FsmRef, Node, After) ->
+ FsmRef ! {migrate_shutdown, Node, After}.
+
%%%----------------------------------------------------------------------
%%% Callback functions from gen_fsm
%%%----------------------------------------------------------------------
@@ -207,7 +211,7 @@ stop(FsmRef) ->
%% ignore |
%% {stop, StopReason}
%%----------------------------------------------------------------------
-init([{SockMod, Socket}, Opts]) ->
+init([{SockMod, Socket}, Opts, FSMLimitOpts]) ->
Access = case lists:keysearch(access, 1, Opts) of
{value, {_, A}} -> A;
_ -> all
@@ -231,7 +235,18 @@ init([{SockMod, Socket}, Opts]) ->
(_) -> false
end, Opts),
TLSOpts = [verify_none | TLSOpts1],
- IP = peerip(SockMod, Socket),
+ Redirect = case lists:keysearch(redirect, 1, Opts) of
+ {value, {_, true}} ->
+ true;
+ _ ->
+ false
+ end,
+ IP = case lists:keysearch(frontend_ip, 1, Opts) of
+ {value, {_, IP1}} ->
+ IP1;
+ _ ->
+ peerip(SockMod, Socket)
+ end,
%% Check if IP is blacklisted:
case is_ip_blacklisted(IP) of
true ->
@@ -241,26 +256,70 @@ init([{SockMod, Socket}, Opts]) ->
false ->
Socket1 =
if
- TLSEnabled ->
+ TLSEnabled andalso SockMod /= ejabberd_frontend_socket ->
SockMod:starttls(Socket, TLSOpts);
true ->
Socket
end,
SocketMonitor = SockMod:monitor(Socket1),
- {ok, wait_for_stream, #state{socket = Socket1,
- sockmod = SockMod,
- socket_monitor = SocketMonitor,
- xml_socket = XMLSocket,
- zlib = Zlib,
- tls = TLS,
- tls_required = StartTLSRequired,
- tls_enabled = TLSEnabled,
- tls_options = TLSOpts,
- streamid = new_id(),
- access = Access,
- shaper = Shaper,
- ip = IP},
- ?C2S_OPEN_TIMEOUT}
+ StateData = #state{socket = Socket1,
+ sockmod = SockMod,
+ socket_monitor = SocketMonitor,
+ xml_socket = XMLSocket,
+ zlib = Zlib,
+ tls = TLS,
+ tls_required = StartTLSRequired,
+ tls_enabled = TLSEnabled,
+ tls_options = TLSOpts,
+ streamid = new_id(),
+ access = Access,
+ shaper = Shaper,
+ ip = IP,
+ redirect = Redirect,
+ fsm_limit_opts = FSMLimitOpts},
+ erlang:send_after(?C2S_OPEN_TIMEOUT, self(), open_timeout),
+ case get_jid_from_opts(Opts) of
+ {ok, #jid{user = U, server = Server, resource = R} = JID} ->
+ ?GEN_FSM:send_event(self(), open_session),
+ {ok, wait_for_session, StateData#state{
+ user = U,
+ server = Server,
+ resource = R,
+ jid = JID,
+ lang = ""}};
+ _ ->
+ {ok, wait_for_stream, StateData, ?C2S_OPEN_TIMEOUT}
+ end
+ end;
+init([StateName, StateData, _FSMLimitOpts]) ->
+ MRef = (StateData#state.sockmod):monitor(StateData#state.socket),
+ if StateName == session_established ->
+ Conn = get_conn_type(StateData),
+ Info = [{ip, StateData#state.ip}, {conn, Conn},
+ {auth_module, StateData#state.auth_module}],
+ {Time, _} = StateData#state.sid,
+ SID = {Time, self()},
+ Priority = case StateData#state.pres_last of
+ undefined ->
+ undefined;
+ El ->
+ get_priority_from_presence(El)
+ end,
+ ejabberd_sm:drop_session(StateData#state.sid),
+ ejabberd_sm:open_session(
+ SID,
+ StateData#state.user,
+ StateData#state.server,
+ StateData#state.resource,
+ Priority,
+ Info),
+ %%ejabberd_sm:drop_session(StateData#state.sid),
+ NewStateData = StateData#state{sid = SID, socket_monitor = MRef},
+ StateData2 = change_reception(NewStateData, true),
+ StateData3 = start_keepalive_timer(StateData2),
+ {ok, StateName, StateData3};
+ true ->
+ {ok, StateName, StateData#state{socket_monitor = MRef}}
end.
%% Return list of all available resources of contacts,
@@ -274,15 +333,25 @@ get_subscribed(FsmRef) ->
%% {stop, Reason, NewStateData}
%%----------------------------------------------------------------------
-wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) ->
+wait_for_stream({xmlstreamstart, Name, Attrs}, StateData) ->
DefaultLang = case ?MYLANG of
undefined ->
"en";
DL ->
DL
end,
- case xml:get_attr_s("xmlns:stream", Attrs) of
- ?NS_STREAM ->
+
+ case {xml:get_attr_s("xmlns:stream", Attrs),
+ xml:get_attr_s("xmlns:flash", Attrs),
+ ?FLASH_HACK,
+ StateData#state.flash_connection} of
+ {_, ?NS_FLASH_STREAM, true, false} ->
+ %% Flash client connecting - attention!
+ %% Some of them don't provide an xmlns:stream attribute -
+ %% compensate for that.
+ wait_for_stream({xmlstreamstart, Name, [{"xmlns:stream", ?NS_STREAM}|Attrs]},
+ StateData#state{flash_connection = true});
+ {?NS_STREAM, _, _, _} ->
Server = jlib:nameprep(xml:get_attr_s("to", Attrs)),
case lists:member(Server, ?MYHOSTS) of
true ->
@@ -362,9 +431,22 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) ->
false ->
[]
end,
+ P1PushFeature =
+ [{xmlelement, "push",
+ [{"xmlns", ?NS_P1_PUSH}], []}],
+ P1RebindFeature =
+ [{xmlelement, "rebind",
+ [{"xmlns", ?NS_P1_REBIND}], []}],
+ P1AckFeature =
+ [{xmlelement, "ack",
+ [{"xmlns", ?NS_P1_ACK}], []}],
send_element(StateData,
{xmlelement, "stream:features", [],
- TLSFeature ++ CompressFeature ++
+ TLSFeature ++
+ CompressFeature ++
+ P1PushFeature ++
+ P1RebindFeature ++
+ P1AckFeature ++
[{xmlelement, "mechanisms",
[{"xmlns", ?NS_SASL}],
Mechs}] ++
@@ -385,7 +467,9 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) ->
roster_get_versioning_feature,
Server, [], [Server]),
StreamFeatures =
- [{xmlelement, "bind",
+ [{xmlelement, "push",
+ [{"xmlns", ?NS_P1_PUSH}], []},
+ {xmlelement, "bind",
[{"xmlns", ?NS_BIND}], []},
{xmlelement, "session",
[{"xmlns", ?NS_SESSION}], []}]
@@ -437,11 +521,17 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) ->
send_trailer(StateData),
{stop, normal, StateData}
end;
- _ ->
- send_header(StateData, ?MYNAME, "", DefaultLang),
- send_element(StateData, ?INVALID_NS_ERR),
- send_trailer(StateData),
- {stop, normal, StateData}
+ _ ->
+ case Name of
+ "policy-file-request" ->
+ send_text(StateData, flash_policy_string()),
+ {stop, normal, StateData};
+ _ ->
+ send_header(StateData, ?MYNAME, "", DefaultLang),
+ send_element(StateData, ?INVALID_NS_ERR),
+ send_trailer(StateData),
+ {stop, normal, StateData}
+ end
end;
wait_for_stream(timeout, StateData) ->
@@ -518,43 +608,55 @@ wait_for_auth({xmlstreamelement, El}, StateData) ->
"(~w) Accepted legacy authentication for ~s by ~p",
[StateData#state.socket,
jlib:jid_to_string(JID), AuthModule]),
- SID = {now(), self()},
- Conn = get_conn_type(StateData),
- Info = [{ip, StateData#state.ip}, {conn, Conn},
- {auth_module, AuthModule}],
- Res1 = jlib:make_result_iq_reply(El),
- Res = setelement(4, Res1, []),
- send_element(StateData, Res),
- ejabberd_sm:open_session(
- SID, U, StateData#state.server, R, Info),
- change_shaper(StateData, JID),
- {Fs, Ts} = ejabberd_hooks:run_fold(
- roster_get_subscription_lists,
- StateData#state.server,
- {[], []},
- [U, StateData#state.server]),
- LJID = jlib:jid_tolower(
- jlib:jid_remove_resource(JID)),
- Fs1 = [LJID | Fs],
- Ts1 = [LJID | Ts],
- PrivList =
- ejabberd_hooks:run_fold(
- privacy_get_user_list, StateData#state.server,
- #userlist{},
- [U, StateData#state.server]),
- NewStateData =
- StateData#state{
- user = U,
- resource = R,
- jid = JID,
- sid = SID,
- conn = Conn,
- auth_module = AuthModule,
- pres_f = ?SETS:from_list(Fs1),
- pres_t = ?SETS:from_list(Ts1),
- privacy_list = PrivList},
- fsm_next_state_pack(session_established,
- NewStateData);
+ case need_redirect(StateData#state{user = U}) of
+ {true, Host} ->
+ ?INFO_MSG("(~w) Redirecting ~s to ~s",
+ [StateData#state.socket,
+ jlib:jid_to_string(JID), Host]),
+ send_element(StateData, ?SERR_SEE_OTHER_HOST(Host)),
+ send_trailer(StateData),
+ {stop, normal, StateData};
+ false ->
+ SID = {now(), self()},
+ Conn = get_conn_type(StateData),
+ Res1 = jlib:make_result_iq_reply(El),
+ Res = setelement(4, Res1, []),
+ send_element(StateData, Res),
+ change_shaper(StateData, JID),
+ {Fs, Ts} = ejabberd_hooks:run_fold(
+ roster_get_subscription_lists,
+ StateData#state.server,
+ {[], []},
+ [U, StateData#state.server]),
+ LJID = jlib:jid_tolower(
+ jlib:jid_remove_resource(JID)),
+ Fs1 = [LJID | Fs],
+ Ts1 = [LJID | Ts],
+ PrivList =
+ ejabberd_hooks:run_fold(
+ privacy_get_user_list,
+ StateData#state.server,
+ #userlist{},
+ [U, StateData#state.server]),
+ NewStateData =
+ StateData#state{
+ user = U,
+ resource = R,
+ jid = JID,
+ sid = SID,
+ conn = Conn,
+ auth_module = AuthModule,
+ pres_f = ?SETS:from_list(Fs1),
+ pres_t = ?SETS:from_list(Ts1),
+ privacy_list = PrivList},
+ DebugFlag = ejabberd_hooks:run_fold(
+ c2s_debug_start_hook,
+ NewStateData#state.server,
+ false,
+ [self(), NewStateData]),
+ maybe_migrate(session_established,
+ NewStateData#state{debug=DebugFlag})
+ end;
_ ->
?INFO_MSG(
"(~w) Failed legacy authentication for ~s",
@@ -586,8 +688,33 @@ wait_for_auth({xmlstreamelement, El}, StateData) ->
end
end;
_ ->
- process_unauthenticated_stanza(StateData, El),
- fsm_next_state(wait_for_auth, StateData)
+ {xmlelement, Name, Attrs, _Els} = El,
+ case {xml:get_attr_s("xmlns", Attrs), Name} of
+ {?NS_P1_REBIND, "rebind"} ->
+ SJID = xml:get_path_s(El, [{elem, "jid"}, cdata]),
+ SID = xml:get_path_s(El, [{elem, "sid"}, cdata]),
+ case jlib:string_to_jid(SJID) of
+ error ->
+ send_element(StateData,
+ {xmlelement, "failure",
+ [{"xmlns", ?NS_P1_REBIND}],
+ [{xmlcdata, "Invalid JID"}]}),
+ fsm_next_state(wait_for_auth,
+ StateData);
+ JID ->
+ case rebind(StateData, JID, SID) of
+ {next_state, wait_for_feature_request,
+ NewStateData, Timeout} ->
+ {next_state, wait_for_auth,
+ NewStateData, Timeout};
+ Res ->
+ Res
+ end
+ end;
+ _ ->
+ process_unauthenticated_stanza(StateData, El),
+ fsm_next_state(wait_for_auth, StateData)
+ end
end;
wait_for_auth(timeout, StateData) ->
@@ -621,21 +748,30 @@ wait_for_feature_request({xmlstreamelement, El}, StateData) ->
Mech,
ClientIn) of
{ok, Props} ->
- (StateData#state.sockmod):reset_stream(
- StateData#state.socket),
- send_element(StateData,
- {xmlelement, "success",
- [{"xmlns", ?NS_SASL}], []}),
- U = xml:get_attr_s(username, Props),
- AuthModule = xml:get_attr_s(auth_module, Props),
- ?INFO_MSG("(~w) Accepted authentication for ~s by ~p",
- [StateData#state.socket, U, AuthModule]),
- fsm_next_state(wait_for_stream,
- StateData#state{
- streamid = new_id(),
- authenticated = true,
- auth_module = AuthModule,
- user = U });
+ catch (StateData#state.sockmod):reset_stream(
+ StateData#state.socket),
+ U = xml:get_attr_s(username, Props),
+ AuthModule = xml:get_attr_s(auth_module, Props),
+ ?INFO_MSG("(~w) Accepted authentication for ~s by ~p",
+ [StateData#state.socket, U, AuthModule]),
+ case need_redirect(StateData#state{user = U}) of
+ {true, Host} ->
+ ?INFO_MSG("(~w) Redirecting ~s to ~s",
+ [StateData#state.socket, U, Host]),
+ send_element(StateData, ?SERR_SEE_OTHER_HOST(Host)),
+ send_trailer(StateData),
+ {stop, normal, StateData};
+ false ->
+ send_element(StateData,
+ {xmlelement, "success",
+ [{"xmlns", ?NS_SASL}], []}),
+ fsm_next_state(wait_for_stream,
+ StateData#state{
+ streamid = new_id(),
+ authenticated = true,
+ auth_module = AuthModule,
+ user = U })
+ end;
{continue, ServerOut, NewSASLState} ->
send_element(StateData,
{xmlelement, "challenge",
@@ -718,6 +854,23 @@ wait_for_feature_request({xmlstreamelement, El}, StateData) ->
StateData)
end
end;
+ {?NS_P1_REBIND, "rebind"} ->
+ SJID = xml:get_path_s(El, [{elem, "jid"}, cdata]),
+ SID = xml:get_path_s(El, [{elem, "sid"}, cdata]),
+ case jlib:string_to_jid(SJID) of
+ error ->
+ send_element(StateData,
+ {xmlelement, "failure",
+ [{"xmlns", ?NS_P1_REBIND}],
+ [{xmlcdata, "Invalid JID"}]}),
+ fsm_next_state(wait_for_feature_request,
+ StateData);
+ JID ->
+ rebind(StateData, JID, SID)
+ end;
+ {?NS_P1_ACK, "ack"} ->
+ fsm_next_state(wait_for_feature_request,
+ StateData#state{ack_enabled = true});
_ ->
if
(SockMod == gen_tcp) and TLSRequired ->
@@ -757,39 +910,57 @@ wait_for_sasl_response({xmlstreamelement, El}, StateData) ->
case cyrsasl:server_step(StateData#state.sasl_state,
ClientIn) of
{ok, Props} ->
- (StateData#state.sockmod):reset_stream(
- StateData#state.socket),
- send_element(StateData,
- {xmlelement, "success",
- [{"xmlns", ?NS_SASL}], []}),
+ catch (StateData#state.sockmod):reset_stream(
+ StateData#state.socket),
U = xml:get_attr_s(username, Props),
AuthModule = xml:get_attr_s(auth_module, Props),
?INFO_MSG("(~w) Accepted authentication for ~s by ~p",
[StateData#state.socket, U, AuthModule]),
- fsm_next_state(wait_for_stream,
- StateData#state{
- streamid = new_id(),
- authenticated = true,
- auth_module = AuthModule,
- user = U});
+ case need_redirect(StateData#state{user = U}) of
+ {true, Host} ->
+ ?INFO_MSG("(~w) Redirecting ~s to ~s",
+ [StateData#state.socket, U, Host]),
+ send_element(StateData, ?SERR_SEE_OTHER_HOST(Host)),
+ send_trailer(StateData),
+ {stop, normal, StateData};
+ false ->
+ send_element(StateData,
+ {xmlelement, "success",
+ [{"xmlns", ?NS_SASL}], []}),
+ fsm_next_state(wait_for_stream,
+ StateData#state{
+ streamid = new_id(),
+ authenticated = true,
+ auth_module = AuthModule,
+ user = U})
+ end;
{ok, Props, ServerOut} ->
(StateData#state.sockmod):reset_stream(
StateData#state.socket),
- send_element(StateData,
- {xmlelement, "success",
- [{"xmlns", ?NS_SASL}],
- [{xmlcdata,
- jlib:encode_base64(ServerOut)}]}),
U = xml:get_attr_s(username, Props),
AuthModule = xml:get_attr_s(auth_module, Props),
?INFO_MSG("(~w) Accepted authentication for ~s by ~p",
[StateData#state.socket, U, AuthModule]),
- fsm_next_state(wait_for_stream,
- StateData#state{
- streamid = new_id(),
- authenticated = true,
- auth_module = AuthModule,
- user = U});
+ case need_redirect(StateData#state{user = U}) of
+ {true, Host} ->
+ ?INFO_MSG("(~w) Redirecting ~s to ~s",
+ [StateData#state.socket, U, Host]),
+ send_element(StateData, ?SERR_SEE_OTHER_HOST(Host)),
+ send_trailer(StateData),
+ {stop, normal, StateData};
+ false ->
+ send_element(StateData,
+ {xmlelement, "success",
+ [{"xmlns", ?NS_SASL}],
+ [{xmlcdata,
+ jlib:encode_base64(ServerOut)}]}),
+ fsm_next_state(wait_for_stream,
+ StateData#state{
+ streamid = new_id(),
+ authenticated = true,
+ auth_module = AuthModule,
+ user = U})
+ end;
{continue, ServerOut, NewSASLState} ->
send_element(StateData,
{xmlelement, "challenge",
@@ -930,7 +1101,7 @@ wait_for_session({xmlstreamelement, El}, StateData) ->
case jlib:iq_query_info(El) of
#iq{type = set, xmlns = ?NS_SESSION} ->
U = StateData#state.user,
- R = StateData#state.resource,
+ %%R = StateData#state.resource,
JID = StateData#state.jid,
case acl:match_rule(StateData#state.server,
StateData#state.access, JID) of
@@ -956,10 +1127,10 @@ wait_for_session({xmlstreamelement, El}, StateData) ->
[U, StateData#state.server]),
SID = {now(), self()},
Conn = get_conn_type(StateData),
- Info = [{ip, StateData#state.ip}, {conn, Conn},
- {auth_module, StateData#state.auth_module}],
- ejabberd_sm:open_session(
- SID, U, StateData#state.server, R, Info),
+ %% Info = [{ip, StateData#state.ip}, {conn, Conn},
+ %% {auth_module, StateData#state.auth_module}],
+ %% ejabberd_sm:open_session(
+ %% SID, U, StateData#state.server, R, Info),
NewStateData =
StateData#state{
sid = SID,
@@ -967,8 +1138,11 @@ wait_for_session({xmlstreamelement, El}, StateData) ->
pres_f = ?SETS:from_list(Fs1),
pres_t = ?SETS:from_list(Ts1),
privacy_list = PrivList},
- fsm_next_state_pack(session_established,
- NewStateData);
+ DebugFlag = ejabberd_hooks:run_fold(c2s_debug_start_hook,
+ NewStateData#state.server,
+ false,
+ [self(), NewStateData]),
+ maybe_migrate(session_established, NewStateData#state{debug=DebugFlag});
_ ->
ejabberd_hooks:run(forbidden_session_hook,
StateData#state.server, [JID]),
@@ -983,6 +1157,11 @@ wait_for_session({xmlstreamelement, El}, StateData) ->
fsm_next_state(wait_for_session, StateData)
end;
+wait_for_session(open_session, StateData) ->
+ El = {xmlelement, "iq", [{"type", "set"}, {"id", "session"}],
+ [{xmlelement, "session", [{"xmlns", ?NS_SESSION}], []}]},
+ wait_for_session({xmlstreamelement, El}, StateData);
+
wait_for_session(timeout, StateData) ->
{stop, normal, StateData};
@@ -1008,7 +1187,9 @@ session_established({xmlstreamelement, El}, StateData) ->
send_trailer(StateData),
{stop, normal, StateData};
_NewEl ->
- session_established2(El, StateData)
+ NSD1 = change_reception(StateData, true),
+ NSD2 = start_keepalive_timer(NSD1),
+ session_established2(El, NSD2)
end;
%% We hibernate the process to reduce memory consumption after a
@@ -1035,7 +1216,16 @@ session_established({xmlstreamerror, _}, StateData) ->
{stop, normal, StateData};
session_established(closed, StateData) ->
- {stop, normal, StateData}.
+ if
+ not StateData#state.reception ->
+ fsm_next_state(session_established, StateData);
+ (StateData#state.keepalive_timer /= undefined) ->
+ NewState1 = change_reception(StateData, false),
+ NewState = start_keepalive_timer(NewState1),
+ fsm_next_state(session_established, NewState);
+ true ->
+ {stop, normal, StateData}
+ end.
%% Process packets sent by user (coming from user on c2s XMPP
%% connection)
@@ -1084,7 +1274,7 @@ session_established2(El, StateData) ->
ejabberd_hooks:run(
user_send_packet,
Server,
- [FromJID, ToJID, PresenceEl]),
+ [StateData#state.debug, FromJID, ToJID, PresenceEl]),
case ToJID of
#jid{user = User,
server = Server,
@@ -1102,23 +1292,35 @@ session_established2(El, StateData) ->
#iq{xmlns = Xmlns} = IQ
when Xmlns == ?NS_PRIVACY;
Xmlns == ?NS_BLOCKING ->
+ ejabberd_hooks:run(
+ user_send_packet,
+ Server,
+ [StateData#state.debug, FromJID, ToJID, NewEl]),
process_privacy_iq(
FromJID, ToJID, IQ, StateData);
+ #iq{xmlns = ?NS_P1_PUSH} = IQ ->
+ process_push_iq(FromJID, ToJID, IQ, StateData);
_ ->
ejabberd_hooks:run(
user_send_packet,
Server,
- [FromJID, ToJID, NewEl]),
+ [StateData#state.debug, FromJID, ToJID, NewEl]),
check_privacy_route(FromJID, StateData, FromJID, ToJID, NewEl),
StateData
end;
"message" ->
ejabberd_hooks:run(user_send_packet,
Server,
- [FromJID, ToJID, NewEl]),
+ [StateData#state.debug, FromJID, ToJID, NewEl]),
check_privacy_route(FromJID, StateData, FromJID,
ToJID, NewEl),
StateData;
+ "standby" ->
+ StandBy = xml:get_tag_cdata(NewEl) == "true",
+ change_standby(StateData, StandBy);
+ "a" ->
+ SCounter = xml:get_tag_attr_s("h", NewEl),
+ receive_ack(StateData, SCounter);
_ ->
StateData
end
@@ -1147,6 +1349,20 @@ session_established2(El, StateData) ->
%% {next_state, NextStateName, NextStateData, Timeout} |
%% {stop, Reason, NewStateData}
%%----------------------------------------------------------------------
+handle_event({add_rosteritem, IJID, ISubscription}, StateName, StateData) ->
+ NewStateData = roster_change(IJID, ISubscription, StateData),
+ fsm_next_state(StateName, NewStateData);
+
+handle_event({del_rosteritem, IJID}, StateName, StateData) ->
+ NewStateData = roster_change(IJID, none, StateData),
+ fsm_next_state(StateName, NewStateData);
+
+handle_event({xmlstreamcdata, _}, session_established = StateName, StateData) ->
+ ?DEBUG("cdata ping", []),
+ NSD1 = change_reception(StateData, true),
+ NSD2 = start_keepalive_timer(NSD1),
+ fsm_next_state(StateName, NSD2);
+
handle_event(_Event, StateName, StateData) ->
fsm_next_state(StateName, StateData).
@@ -1339,6 +1555,17 @@ handle_info({route, From, To, Packet}, StateName, StateData) ->
_ ->
{false, Attrs, StateData}
end;
+ rebind ->
+ {Pid2, StreamID2} = Els,
+ if
+ StreamID2 == StateData#state.streamid ->
+ Pid2 ! {rebind, prepare_acks_for_rebind(StateData)},
+ receive after 1000 -> ok end,
+ {exit, Attrs, rebind};
+ true ->
+ Pid2 ! {rebind, false},
+ {false, Attrs, StateData}
+ end;
"iq" ->
IQ = jlib:iq_query_info(Packet),
case IQ of
@@ -1378,7 +1605,23 @@ handle_info({route, From, To, Packet}, StateName, StateData) ->
"message" ->
case privacy_check_packet(StateData, From, To, Packet, in) of
allow ->
- {true, Attrs, StateData};
+ if StateData#state.reception ->
+ case ejabberd_hooks:run_fold(
+ feature_check_packet, StateData#state.server,
+ allow,
+ [StateData#state.jid,
+ StateData#state.server,
+ StateData#state.pres_last,
+ {From, To, Packet},
+ in]) of
+ allow ->
+ {true, Attrs, StateData};
+ deny ->
+ {false, Attrs, StateData}
+ end;
+ true ->
+ {true, Attrs, StateData}
+ end;
deny ->
{false, Attrs, StateData}
end;
@@ -1387,29 +1630,92 @@ handle_info({route, From, To, Packet}, StateName, 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};
+ catch send_trailer(StateData),
+ case NewState of
+ rebind ->
+ {stop, normal, StateData#state{authenticated = rebinded}};
+ _ ->
+ {stop, normal, StateData}
+ end;
Pass ->
Attrs2 = jlib:replace_from_to_attrs(jlib:jid_to_string(From),
jlib:jid_to_string(To),
NewAttrs),
FixedPacket = {xmlelement, Name, Attrs2, Els},
- send_element(StateData, FixedPacket),
+ NewState2 =
+ if
+ NewState#state.reception and
+ not (NewState#state.standby and (Name /= "message")) ->
+ send_element(NewState, FixedPacket),
+ ack(NewState, From, To, FixedPacket);
+ true ->
+ NewState1 = send_out_of_reception_message(
+ NewState, From, To, Packet),
+ enqueue(NewState1, From, To, FixedPacket)
+ end,
ejabberd_hooks:run(user_receive_packet,
StateData#state.server,
- [StateData#state.jid, From, To, FixedPacket]),
+ [StateData#state.debug, StateData#state.jid, From, To, FixedPacket]),
ejabberd_hooks:run(c2s_loop_debug, [{route, From, To, Packet}]),
- fsm_next_state(StateName, NewState);
+ fsm_next_state(StateName, NewState2);
true ->
ejabberd_hooks:run(c2s_loop_debug, [{route, From, To, Packet}]),
fsm_next_state(StateName, NewState)
end;
-handle_info({'DOWN', Monitor, _Type, _Object, _Info}, _StateName, StateData)
- when Monitor == StateData#state.socket_monitor ->
+handle_info({timeout, Timer, _}, StateName,
+ #state{keepalive_timer = Timer, reception = true} = StateData) ->
+ NewState1 = change_reception(StateData, false),
+ NewState = start_keepalive_timer(NewState1),
+ fsm_next_state(StateName, NewState);
+handle_info({timeout, Timer, _}, _StateName,
+ #state{keepalive_timer = Timer, reception = false} = StateData) ->
{stop, normal, StateData};
+handle_info({timeout, Timer, PrevCounter}, StateName,
+ #state{ack_timer = Timer} = StateData) ->
+ AckCounter = StateData#state.ack_counter,
+ NewState =
+ if
+ PrevCounter >= AckCounter ->
+ StateData#state{ack_timer = undefined};
+ true ->
+ send_ack_request(StateData#state{ack_timer = undefined})
+ end,
+ fsm_next_state(StateName, NewState);
+handle_info({ack_timeout, Counter}, StateName, StateData) ->
+ AckQueue = StateData#state.ack_queue,
+ case queue:is_empty(AckQueue) of
+ true ->
+ fsm_next_state(StateName, StateData);
+ false ->
+ C = element(1, queue:head(AckQueue)),
+ if
+ C =< Counter ->
+ {stop, normal, StateData};
+ true ->
+ fsm_next_state(StateName, StateData)
+ end
+ end;
+handle_info(open_timeout, StateName, StateData) ->
+ case StateName of
+ session_established ->
+ fsm_next_state(StateName, StateData);
+ _ ->
+ {stop, normal, StateData}
+ end;
+handle_info({'DOWN', Monitor, _Type, _Object, _Info}, StateName, StateData)
+ when Monitor == StateData#state.socket_monitor ->
+ if
+ (StateName == session_established) and
+ (not StateData#state.reception) ->
+ fsm_next_state(StateName, StateData);
+ (StateName == session_established) and
+ (StateData#state.keepalive_timer /= undefined) ->
+ NewState1 = change_reception(StateData, false),
+ NewState = start_keepalive_timer(NewState1),
+ fsm_next_state(StateName, NewState);
+ true ->
+ {stop, normal, StateData}
+ end;
handle_info(system_shutdown, StateName, StateData) ->
case StateName of
wait_for_stream ->
@@ -1442,6 +1748,22 @@ handle_info({force_update_presence, LUser}, StateName,
StateData
end,
{next_state, StateName, NewStateData};
+handle_info({migrate, Node}, StateName, StateData) ->
+ if Node /= node() ->
+ fsm_migrate(StateName, StateData, Node, 0);
+ true ->
+ fsm_next_state(StateName, StateData)
+ end;
+handle_info({migrate_shutdown, Node, After}, StateName, StateData) ->
+ case StateData#state.sockmod == ejabberd_frontend_socket orelse
+ StateData#state.xml_socket == true orelse
+ is_remote_receiver(StateData#state.socket) of
+ true ->
+ migrate(self(), Node, After);
+ false ->
+ self() ! system_shutdown
+ end,
+ fsm_next_state(StateName, StateData);
handle_info({broadcast, Type, From, Packet}, StateName, StateData) ->
Recipients = ejabberd_hooks:run_fold(
c2s_broadcast_recipients, StateData#state.server,
@@ -1453,6 +1775,14 @@ handle_info({broadcast, Type, From, Packet}, StateName, StateData) ->
From, jlib:make_jid(USR), Packet)
end, lists:usort(Recipients)),
fsm_next_state(StateName, StateData);
+handle_info({change_socket, Socket}, StateName, StateData) ->
+ erlang:demonitor(StateData#state.socket_monitor),
+ NewSocket = (StateData#state.sockmod):change_socket(
+ StateData#state.socket, Socket),
+ MRef = (StateData#state.sockmod):monitor(NewSocket),
+ fsm_next_state(StateName,
+ StateData#state{socket = NewSocket,
+ socket_monitor = MRef});
handle_info(Info, StateName, StateData) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
fsm_next_state(StateName, StateData).
@@ -1469,12 +1799,31 @@ print_state(State = #state{pres_t = T, pres_f = F, pres_a = A, pres_i = I}) ->
pres_a = {pres_a, ?SETS:size(A)},
pres_i = {pres_i, ?SETS:size(I)}
}.
-
+
%%----------------------------------------------------------------------
%% Func: terminate/3
%% Purpose: Shutdown the fsm
%% Returns: any
%%----------------------------------------------------------------------
+terminate({migrated, ClonePid}, StateName, StateData) ->
+ ejabberd_hooks:run(c2s_debug_stop_hook,
+ StateData#state.server,
+ [self(), StateData]),
+ if StateName == session_established ->
+ ?INFO_MSG("(~w) Migrating ~s to ~p on node ~p",
+ [StateData#state.socket,
+ jlib:jid_to_string(StateData#state.jid),
+ ClonePid, node(ClonePid)]),
+ ejabberd_sm:close_migrated_session(StateData#state.sid,
+ StateData#state.user,
+ StateData#state.server,
+ StateData#state.resource);
+ true ->
+ ok
+ end,
+ (StateData#state.sockmod):change_controller(
+ StateData#state.socket, ClonePid),
+ ok;
terminate(_Reason, StateName, StateData) ->
case StateName of
session_established ->
@@ -1498,6 +1847,13 @@ terminate(_Reason, StateName, StateData) ->
StateData, From, StateData#state.pres_a, Packet),
presence_broadcast(
StateData, From, StateData#state.pres_i, Packet);
+ rebinded ->
+ ejabberd_sm:close_migrated_session(
+ StateData#state.sid,
+ StateData#state.user,
+ StateData#state.server,
+ StateData#state.resource),
+ ok;
_ ->
?INFO_MSG("(~w) Close session for ~s",
[StateData#state.socket,
@@ -1529,6 +1885,36 @@ terminate(_Reason, StateName, StateData) ->
StateData, From, StateData#state.pres_i, Packet)
end
end,
+ case StateData#state.authenticated of
+ rebinded ->
+ ok;
+ _ ->
+ if
+ not StateData#state.reception, not StateData#state.oor_offline ->
+ SFrom = jlib:jid_to_string(StateData#state.jid),
+ ejabberd_hooks:run(
+ p1_push_notification,
+ StateData#state.server,
+ [StateData#state.server,
+ StateData#state.jid,
+ StateData#state.oor_notification,
+ "Instant messaging session expired",
+ 0,
+ false,
+ StateData#state.oor_appid,
+ SFrom]);
+ true ->
+ ok
+ end,
+ lists:foreach(
+ fun({_Counter, From, To, FixedPacket}) ->
+ ejabberd_router:route(From, To, FixedPacket)
+ end, queue:to_list(StateData#state.ack_queue)),
+ lists:foreach(
+ fun({From, To, FixedPacket}) ->
+ ejabberd_router:route(From, To, FixedPacket)
+ end, queue:to_list(StateData#state.queue))
+ end,
bounce_messages();
_ ->
ok
@@ -1547,18 +1933,44 @@ change_shaper(StateData, JID) ->
send_text(StateData, Text) when StateData#state.xml_socket ->
?DEBUG("Send Text on stream = ~p", [lists:flatten(Text)]),
- (StateData#state.sockmod):send_xml(StateData#state.socket,
+ (StateData#state.sockmod):send_xml(StateData#state.socket,
{xmlstreamraw, Text});
send_text(StateData, Text) ->
?DEBUG("Send XML on stream = ~p", [Text]),
- (StateData#state.sockmod):send(StateData#state.socket, Text).
+ Text1 =
+ if ?FLASH_HACK and StateData#state.flash_connection ->
+ %% send a null byte after each stanza to Flash clients
+ [Text, 0];
+ true ->
+ Text
+ end,
+ (StateData#state.sockmod):send(StateData#state.socket, Text1).
send_element(StateData, El) when StateData#state.xml_socket ->
+ ejabberd_hooks:run(feature_inspect_packet,
+ StateData#state.server,
+ [StateData#state.jid,
+ StateData#state.server,
+ StateData#state.pres_last, El]),
(StateData#state.sockmod):send_xml(StateData#state.socket,
{xmlstreamelement, El});
send_element(StateData, El) ->
+ ejabberd_hooks:run(feature_inspect_packet,
+ StateData#state.server,
+ [StateData#state.jid,
+ StateData#state.server,
+ StateData#state.pres_last, El]),
send_text(StateData, xml:element_to_binary(El)).
+send_header(StateData,Server, Version, Lang)
+ when StateData#state.flash_connection ->
+ Header = io_lib:format(?FLASH_STREAM_HEADER,
+ [StateData#state.streamid,
+ Server,
+ Version,
+ Lang]),
+ send_text(StateData, Header);
+
send_header(StateData, Server, Version, Lang)
when StateData#state.xml_socket ->
VersionAttr =
@@ -1647,16 +2059,22 @@ get_auth_tags([], U, P, D, R) ->
get_conn_type(StateData) ->
case (StateData#state.sockmod):get_sockmod(StateData#state.socket) of
- gen_tcp -> c2s;
- tls -> c2s_tls;
- ejabberd_zlib ->
- case ejabberd_zlib:get_sockmod((StateData#state.socket)#socket_state.socket) of
- gen_tcp -> c2s_compressed;
- tls -> c2s_compressed_tls
- end;
- ejabberd_http_poll -> http_poll;
- ejabberd_http_bind -> http_bind;
- _ -> unknown
+ gen_tcp -> c2s;
+ tls -> c2s_tls;
+ ejabberd_zlib ->
+ if is_pid(StateData#state.socket) ->
+ unknown;
+ true ->
+ case ejabberd_zlib:get_sockmod(
+ (StateData#state.socket)#socket_state.socket) of
+ gen_tcp -> c2s_compressed;
+ tls -> c2s_compressed_tls
+ end
+ end;
+ ejabberd_http_poll -> http_poll;
+ ejabberd_http_ws -> http_ws;
+ ejabberd_http_bind -> http_bind;
+ _ -> unknown
end.
process_presence_probe(From, To, StateData) ->
@@ -1681,14 +2099,40 @@ process_presence_probe(From, To, StateData) ->
andalso ?SETS:is_element(LFrom, StateData#state.pres_a),
if
Cond1 ->
+ Packet =
+ case StateData#state.reception of
+ true ->
+ StateData#state.pres_last;
+ false ->
+ case StateData#state.oor_show of
+ "" ->
+ StateData#state.pres_last;
+ _ ->
+ {xmlelement, _, PresAttrs, PresEls} =
+ StateData#state.pres_last,
+ PresEls1 =
+ lists:flatmap(
+ fun({xmlelement, Name, _, _})
+ when Name == "show";
+ Name == "status" ->
+ [];
+ (E) ->
+ [E]
+ end, PresEls),
+ make_oor_presence(
+ StateData, PresAttrs, PresEls1)
+ end
+ end,
Timestamp = StateData#state.pres_timestamp,
- Packet = xml:append_subtags(
- StateData#state.pres_last,
- %% To is the one sending the presence (the target of the probe)
- [jlib:timestamp_to_xml(Timestamp, utc, To, ""),
- %% TODO: Delete the next line once XEP-0091 is Obsolete
- jlib:timestamp_to_xml(Timestamp)]),
- case privacy_check_packet(StateData, To, From, Packet, out) of
+ Packet1 = maybe_add_delay(Packet, utc, To, "", Timestamp),
+ case ejabberd_hooks:run_fold(
+ privacy_check_packet, StateData#state.server,
+ allow,
+ [StateData#state.user,
+ StateData#state.server,
+ StateData#state.privacy_list,
+ {To, From, Packet1},
+ out]) of
deny ->
ok;
allow ->
@@ -1697,7 +2141,7 @@ process_presence_probe(From, To, StateData) ->
%% Don't route a presence probe to oneself
case From == To of
false ->
- ejabberd_router:route(To, From, Packet);
+ ejabberd_router:route(To, From, Packet1);
true ->
ok
end
@@ -1999,6 +2443,7 @@ roster_change(IJID, ISubscription, StateData) ->
?DEBUG("roster changed for ~p~n", [StateData#state.user]),
From = StateData#state.jid,
To = jlib:make_jid(IJID),
+% To = IJID,
Cond1 = (not StateData#state.pres_invis) and IsFrom
and (not OldIsFrom),
Cond2 = (not IsFrom) and OldIsFrom
@@ -2043,8 +2488,15 @@ roster_change(IJID, ISubscription, StateData) ->
update_priority(Priority, Packet, StateData) ->
- Info = [{ip, StateData#state.ip}, {conn, StateData#state.conn},
- {auth_module, StateData#state.auth_module}],
+ Info1 = [{ip, StateData#state.ip}, {conn, StateData#state.conn},
+ {auth_module, StateData#state.auth_module}],
+ Info =
+ case StateData#state.reception of
+ false ->
+ [{oor, true} | Info1];
+ _ ->
+ Info1
+ end,
ejabberd_sm:set_presence(StateData#state.sid,
StateData#state.user,
StateData#state.server,
@@ -2123,7 +2575,7 @@ resend_offline_messages(StateData) ->
%% Attrs),
%% FixedPacket = {xmlelement, Name, Attrs2, Els},
%% Use route instead of send_element to go through standard workflow
- ejabberd_router:route(From, To, Packet);
+ ejabberd_router:route(From, To, Packet);
%% send_element(StateData, FixedPacket),
%% ejabberd_hooks:run(user_receive_packet,
%% StateData#state.server,
@@ -2210,16 +2662,30 @@ peerip(SockMod, Socket) ->
_ -> undefined
end.
-%% fsm_next_state_pack: Pack the StateData structure to improve
-%% sharing.
-fsm_next_state_pack(StateName, StateData) ->
- fsm_next_state_gc(StateName, pack(StateData)).
-
-%% fsm_next_state_gc: Garbage collect the process heap to make use of
-%% the newly packed StateData structure.
-fsm_next_state_gc(StateName, PackedStateData) ->
- erlang:garbage_collect(),
- fsm_next_state(StateName, PackedStateData).
+maybe_migrate(StateName, StateData) ->
+ PackedStateData = pack(StateData),
+ #state{user = U, server = S, resource = R, sid = SID} = StateData,
+ case ejabberd_cluster:get_node({jlib:nodeprep(U), jlib:nameprep(S)}) of
+ Node when Node == node() ->
+ Conn = get_conn_type(StateData),
+ Info = [{ip, StateData#state.ip}, {conn, Conn},
+ {auth_module, StateData#state.auth_module}],
+ Presence = StateData#state.pres_last,
+ Priority =
+ case Presence of
+ undefined ->
+ undefined;
+ _ ->
+ get_priority_from_presence(Presence)
+ end,
+ ejabberd_sm:open_session(SID, U, S, R, Priority, Info),
+ StateData2 = change_reception(PackedStateData, true),
+ StateData3 = start_keepalive_timer(StateData2),
+ erlang:garbage_collect(),
+ fsm_next_state(StateName, StateData3);
+ Node ->
+ fsm_migrate(StateName, PackedStateData, Node, 0)
+ end.
%% fsm_next_state: Generate the next_state FSM tuple with different
%% timeout, depending on the future state
@@ -2228,6 +2694,10 @@ fsm_next_state(session_established, StateData) ->
fsm_next_state(StateName, StateData) ->
{next_state, StateName, StateData, ?C2S_OPEN_TIMEOUT}.
+fsm_migrate(StateName, StateData, Node, Timeout) ->
+ {migrate, StateData,
+ {Node, ?MODULE, start, [StateName, StateData]}, Timeout}.
+
%% fsm_reply: Generate the reply FSM tuple with different timeout,
%% depending on the future state
fsm_reply(Reply, session_established, StateData) ->
@@ -2268,6 +2738,710 @@ check_from(El, FromJID) ->
end
end.
+start_keepalive_timer(StateData) ->
+ if
+ is_reference(StateData#state.keepalive_timer) ->
+ cancel_timer(StateData#state.keepalive_timer);
+ true ->
+ ok
+ end,
+ Timeout =
+ if
+ StateData#state.reception -> StateData#state.keepalive_timeout;
+ true -> StateData#state.oor_timeout
+ end,
+ Timer =
+ if
+ is_integer(Timeout) ->
+ erlang:start_timer(Timeout * 1000, self(), []);
+ true ->
+ undefined
+ end,
+ StateData#state{keepalive_timer = Timer}.
+
+change_reception(#state{reception = Reception} = StateData, Reception) ->
+ StateData;
+change_reception(#state{reception = true} = StateData, false) ->
+ ?DEBUG("reception -> false", []),
+ case StateData#state.oor_show of
+ "" ->
+ ok;
+ _ ->
+ Packet = make_oor_presence(StateData),
+ update_priority(0, Packet, StateData#state{reception = false}),
+ presence_broadcast_to_trusted(
+ StateData,
+ StateData#state.jid,
+ StateData#state.pres_f,
+ StateData#state.pres_a,
+ Packet)
+ end,
+ StateData#state{reception = false};
+change_reception(#state{reception = false, standby = true} = StateData, true) ->
+ ?DEBUG("reception -> standby", []),
+ NewQueue =
+ lists:foldl(
+ fun({_From, _To, {xmlelement, "message", _, _} = FixedPacket}, Q) ->
+ send_element(StateData, FixedPacket),
+ Q;
+ (Item, Q) ->
+ queue:in(Item, Q)
+ end, queue:new(), queue:to_list(StateData#state.queue)),
+ StateData#state{queue = NewQueue,
+ queue_len = queue:len(NewQueue),
+ reception = true,
+ oor_unread = 0,
+ oor_unread_users = ?SETS:new()};
+change_reception(#state{reception = false} = StateData, true) ->
+ ?DEBUG("reception -> true", []),
+ case StateData#state.oor_show of
+ "" ->
+ ok;
+ _ ->
+ Packet = StateData#state.pres_last,
+ NewPriority = get_priority_from_presence(Packet),
+ update_priority(NewPriority, Packet,
+ StateData#state{reception = true}),
+ presence_broadcast_to_trusted(
+ StateData,
+ StateData#state.jid,
+ StateData#state.pres_f,
+ StateData#state.pres_a,
+ Packet)
+ end,
+ lists:foreach(
+ fun({_From, _To, FixedPacket}) ->
+ send_element(StateData, FixedPacket)
+ end, queue:to_list(StateData#state.queue)),
+ lists:foreach(
+ fun(FixedPacket) ->
+ send_element(StateData, FixedPacket)
+ end, gb_trees:values(StateData#state.pres_queue)),
+ StateData#state{queue = queue:new(),
+ queue_len = 0,
+ pres_queue = gb_trees:empty(),
+ reception = true,
+ oor_unread = 0,
+ oor_unread_users = ?SETS:new()}.
+
+change_standby(#state{standby = StandBy} = StateData, StandBy) ->
+ StateData;
+change_standby(#state{standby = false} = StateData, true) ->
+ ?DEBUG("standby -> true", []),
+ StateData#state{standby = true};
+change_standby(#state{standby = true} = StateData, false) ->
+ ?DEBUG("standby -> false", []),
+ lists:foreach(
+ fun({_From, _To, FixedPacket}) ->
+ send_element(StateData, FixedPacket)
+ end, queue:to_list(StateData#state.queue)),
+ lists:foreach(
+ fun(FixedPacket) ->
+ send_element(StateData, FixedPacket)
+ end, gb_trees:values(StateData#state.pres_queue)),
+ StateData#state{queue = queue:new(),
+ queue_len = 0,
+ pres_queue = gb_trees:empty(),
+ standby = false}.
+
+send_out_of_reception_message(StateData, From, To,
+ {xmlelement, "message", _, _} = Packet) ->
+ Type = xml:get_tag_attr_s("type", Packet),
+ if
+ (Type == "normal") or
+ (Type == "") or
+ (Type == "chat") or
+ (StateData#state.oor_send_groupchat and (Type == "groupchat"))->
+ %Lang = case xml:get_tag_attr_s("xml:lang", Packet) of
+ % "" ->
+ % StateData#state.lang;
+ % L ->
+ % L
+ % end,
+ %Text = translate:translate(
+ % Lang, "User is temporarily out of reception"),
+ %MsgType = "error",
+ %Message = {xmlelement, "message",
+ % [{"type", MsgType}],
+ % [{xmlelement, "body", [],
+ % [{xmlcdata, Text}]}]},
+ %ejabberd_router:route(To, From, Message),
+ Body1 = xml:get_path_s(Packet, [{elem, "body"}, cdata]),
+ Body =
+ case check_x_attachment(Packet) of
+ true ->
+ case Body1 of
+ "" -> [238, 128, 136];
+ _ ->
+ [238, 128, 136, 32 | Body1]
+ end;
+ false ->
+ Body1
+ end,
+ Pushed = check_x_pushed(Packet),
+ if
+ Body == "";
+ Pushed ->
+ StateData;
+ true ->
+ BFrom = jlib:jid_remove_resource(From),
+ LBFrom = jlib:jid_tolower(BFrom),
+ UnreadUsers = ?SETS:add_element(
+ LBFrom,
+ StateData#state.oor_unread_users),
+ IncludeBody =
+ case StateData#state.oor_send_body of
+ all ->
+ true;
+ first_per_user ->
+ not ?SETS:is_element(
+ LBFrom,
+ StateData#state.oor_unread_users);
+ first ->
+ StateData#state.oor_unread == 0;
+ none ->
+ false
+ end,
+ Unread = StateData#state.oor_unread + 1,
+ SFrom = jlib:jid_to_string(BFrom),
+ Msg =
+ if
+ IncludeBody ->
+ CBody = utf8_cut(Body, 100),
+ case StateData#state.oor_send_from of
+ jid -> SFrom ++ ": " ++ CBody;
+ username ->
+ UnescapedFrom =
+ unescape(BFrom#jid.user),
+ UnescapedFrom ++ ": " ++ CBody;
+ name ->
+ Name = get_roster_name(
+ StateData, BFrom),
+ Name ++ ": " ++ CBody;
+ _ -> CBody
+ end;
+ true ->
+ ""
+ end,
+ Sound = IncludeBody,
+ AppID = StateData#state.oor_appid,
+ ejabberd_hooks:run(
+ p1_push_notification,
+ StateData#state.server,
+ [StateData#state.server,
+ StateData#state.jid,
+ StateData#state.oor_notification,
+ Msg,
+ Unread + StateData#state.oor_unread_client,
+ Sound,
+ AppID,
+ SFrom]),
+ %% This hook is intended to give other module a
+ %% chance to notify the sender that the message is
+ %% not directly delivered to the client (almost
+ %% equivalent to offline).
+ ejabberd_hooks:run(delayed_message_hook,
+ StateData#state.server,
+ [From, To, Packet]),
+ StateData#state{oor_unread = Unread,
+ oor_unread_users = UnreadUsers}
+ end;
+ true ->
+ StateData
+ end;
+send_out_of_reception_message(StateData, _From, _To, _Packet) ->
+ StateData.
+
+make_oor_presence(StateData) ->
+ make_oor_presence(StateData, [], []).
+
+make_oor_presence(StateData, PresenceAttrs, PresenceEls) ->
+ ShowEl =
+ case StateData#state.oor_show of
+ "available" -> [];
+ _ ->
+ [{xmlelement, "show", [],
+ [{xmlcdata, StateData#state.oor_show}]}]
+ end,
+ {xmlelement, "presence", PresenceAttrs,
+ ShowEl ++
+ [{xmlelement, "status", [],
+ [{xmlcdata, StateData#state.oor_status}]}]
+ ++ PresenceEls}.
+
+utf8_cut(S, Bytes) ->
+ utf8_cut(S, [], [], Bytes + 1).
+
+utf8_cut(_S, _Cur, Prev, 0) ->
+ lists:reverse(Prev);
+utf8_cut([], Cur, _Prev, _Bytes) ->
+ lists:reverse(Cur);
+utf8_cut([C | S], Cur, Prev, Bytes) ->
+ if
+ C bsr 6 == 2 ->
+ utf8_cut(S, [C | Cur], Prev, Bytes - 1);
+ true ->
+ utf8_cut(S, [C | Cur], Cur, Bytes - 1)
+ end.
+
+-include("mod_roster.hrl").
+
+get_roster_name(StateData, JID) ->
+ User = StateData#state.user,
+ Server = StateData#state.server,
+ RosterItems = ejabberd_hooks:run_fold(
+ roster_get, Server, [], [{User, Server}]),
+ JUser = JID#jid.luser,
+ JServer = JID#jid.lserver,
+ Item =
+ lists:foldl(
+ fun(_, Res = #roster{}) ->
+ Res;
+ (I, false) ->
+ case I#roster.jid of
+ {JUser, JServer, _} ->
+ I;
+ _ ->
+ false
+ end
+ end, false, RosterItems),
+ case Item of
+ false ->
+ unescape(JID#jid.user);
+ #roster{} ->
+ Item#roster.name
+ end.
+
+unescape("") -> "";
+unescape("\\20" ++ S) -> [$\s | unescape(S)];
+unescape("\\22" ++ S) -> [$" | unescape(S)];
+unescape("\\26" ++ S) -> [$& | unescape(S)];
+unescape("\\27" ++ S) -> [$' | unescape(S)];
+unescape("\\2f" ++ S) -> [$/ | unescape(S)];
+unescape("\\3a" ++ S) -> [$: | unescape(S)];
+unescape("\\3c" ++ S) -> [$< | unescape(S)];
+unescape("\\3e" ++ S) -> [$> | unescape(S)];
+unescape("\\40" ++ S) -> [$@ | unescape(S)];
+unescape("\\5c" ++ S) -> [$\\ | unescape(S)];
+unescape([C | S]) -> [C | unescape(S)].
+
+
+cancel_timer(Timer) ->
+ erlang:cancel_timer(Timer),
+ receive
+ {timeout, Timer, _} ->
+ ok
+ after 0 ->
+ ok
+ end.
+
+enqueue(StateData, From, To, Packet) ->
+ IsPresence =
+ case Packet of
+ {xmlelement, "presence", _, _} ->
+ case xml:get_tag_attr_s("type", Packet) of
+ "subscribe" ->
+ false;
+ "subscribed" ->
+ false;
+ "unsubscribe" ->
+ false;
+ "unsubscribed" ->
+ false;
+ _ ->
+ true
+ end;
+ _ ->
+ false
+ end,
+ Messages =
+ StateData#state.queue_len + gb_trees:size(StateData#state.pres_queue),
+ if
+ Messages >= ?MAX_OOR_MESSAGES ->
+ self() ! {timeout, StateData#state.keepalive_timer, []};
+ true ->
+ ok
+ end,
+ if
+ IsPresence ->
+ LFrom = jlib:jid_tolower(From),
+ case is_own_presence(StateData#state.jid, LFrom) of
+ true -> StateData;
+ false ->
+ NewQueue = gb_trees:enter(LFrom, Packet,
+ StateData#state.pres_queue),
+ StateData#state{pres_queue = NewQueue}
+ end;
+ true ->
+ CleanPacket = xml:remove_subtags(Packet, "x", {"xmlns", ?NS_P1_PUSHED}),
+ Packet2 =
+ case CleanPacket of
+ {xmlelement, "message", _, _} ->
+ xml:append_subtags(
+ maybe_add_delay(CleanPacket, utc, To, ""),
+ [{xmlelement, "x", [{"xmlns", ?NS_P1_PUSHED}], []}]);
+ _ ->
+ Packet
+ end,
+ NewQueue = queue:in({From, To, Packet2},
+ StateData#state.queue),
+ NewQueueLen = StateData#state.queue_len + 1,
+ StateData#state{queue = NewQueue,
+ queue_len = NewQueueLen}
+ end.
+
+%% Is my own presence packet ?
+is_own_presence(MyFullJID, MyFullJID) ->
+ true;
+is_own_presence(_MyFullJID, _LFrom) ->
+ false.
+
+ack(StateData, From, To, Packet) ->
+ if
+ StateData#state.ack_enabled ->
+ NeedsAck =
+ case Packet of
+ {xmlelement, "presence", _, _} ->
+ case xml:get_tag_attr_s("type", Packet) of
+ "subscribe" ->
+ true;
+ "subscribed" ->
+ true;
+ "unsubscribe" ->
+ true;
+ "unsubscribed" ->
+ true;
+ _ ->
+ false
+ end;
+ {xmlelement, "message", _, _} ->
+ true;
+ _ ->
+ false
+ end,
+ if
+ NeedsAck ->
+ Counter = StateData#state.ack_counter + 1,
+ NewAckQueue = queue:in({Counter, From, To, Packet},
+ StateData#state.ack_queue),
+ send_ack_request(StateData#state{ack_queue = NewAckQueue,
+ ack_counter = Counter});
+ true ->
+ StateData
+ end;
+ true ->
+ StateData
+ end.
+
+send_ack_request(StateData) ->
+ case StateData#state.ack_timer of
+ undefined ->
+ AckCounter = StateData#state.ack_counter,
+ AckTimer =
+ erlang:start_timer(?C2S_P1_ACK_TIMEOUT, self(), AckCounter),
+ AckTimeout = StateData#state.keepalive_timeout +
+ StateData#state.oor_timeout,
+ erlang:send_after(AckTimeout * 1000, self(),
+ {ack_timeout, AckTimeout}),
+ send_element(
+ StateData,
+ {xmlelement, "r",
+ [{"h", integer_to_list(AckCounter)}], []}),
+ StateData#state{ack_timer = AckTimer};
+ _ ->
+ StateData
+ end.
+
+receive_ack(StateData, SCounter) ->
+ case catch list_to_integer(SCounter) of
+ Counter when is_integer(Counter) ->
+ NewQueue = clean_queue(StateData#state.ack_queue, Counter),
+ StateData#state{ack_queue = NewQueue};
+ _ ->
+ StateData
+ end.
+
+clean_queue(Queue, Counter) ->
+ case queue:is_empty(Queue) of
+ true ->
+ Queue;
+ false ->
+ C = element(1, queue:head(Queue)),
+ if
+ C =< Counter ->
+ clean_queue(queue:tail(Queue), Counter);
+ true ->
+ Queue
+ end
+ end.
+
+prepare_acks_for_rebind(StateData) ->
+ AckQueue = StateData#state.ack_queue,
+ case queue:is_empty(AckQueue) of
+ true ->
+ StateData;
+ false ->
+ Unsent =
+ lists:map(
+ fun({_Counter, From, To, FixedPacket}) ->
+ {From, To, FixedPacket}
+ end, queue:to_list(AckQueue)),
+ NewQueue = queue:join(queue:from_list(Unsent),
+ StateData#state.queue),
+ StateData#state{queue = NewQueue,
+ queue_len = queue:len(NewQueue),
+ ack_queue = queue:new(),
+ reception = false}
+ end.
+
+
+rebind(StateData, JID, StreamID) ->
+ case JID#jid.lresource of
+ "" ->
+ send_element(StateData,
+ {xmlelement, "failure",
+ [{"xmlns", ?NS_P1_REBIND}],
+ [{xmlcdata, "Invalid JID"}]}),
+ fsm_next_state(wait_for_feature_request,
+ StateData);
+ _ ->
+ ejabberd_sm:route(
+ ?MODULE, JID,
+ {xmlelement, rebind, [], {self(), StreamID}}),
+ receive
+ {rebind, false} ->
+ send_element(StateData,
+ {xmlelement, "failure",
+ [{"xmlns", ?NS_P1_REBIND}],
+ [{xmlcdata, "Session not found"}]}),
+ fsm_next_state(wait_for_feature_request,
+ StateData);
+ {rebind, NewStateData} ->
+ ?INFO_MSG("(~w) Reopened session for ~s",
+ [StateData#state.socket,
+ jlib:jid_to_string(JID)]),
+ SID = {now(), self()},
+ StateData2 =
+ NewStateData#state{
+ socket = StateData#state.socket,
+ sockmod = StateData#state.sockmod,
+ socket_monitor = StateData#state.socket_monitor,
+ sid = SID,
+ ip = StateData#state.ip,
+ keepalive_timer = StateData#state.keepalive_timer,
+ ack_timer = undefined
+ },
+ send_element(StateData2,
+ {xmlelement, "rebind",
+ [{"xmlns", ?NS_P1_REBIND}],
+ []}),
+ maybe_migrate(session_established, StateData2)
+ after 1000 ->
+ send_element(StateData,
+ {xmlelement, "failure",
+ [{"xmlns", ?NS_P1_REBIND}],
+ [{xmlcdata, "Session not found"}]}),
+ fsm_next_state(wait_for_feature_request,
+ StateData)
+ end
+ end.
+
+process_push_iq(From, To,
+ #iq{type = _Type, sub_el = El} = IQ,
+ StateData) ->
+ {Res, NewStateData} =
+ case El of
+ {xmlelement, "push", _, _} ->
+ SKeepAlive =
+ xml:get_path_s(El, [{elem, "keepalive"}, {attr, "max"}]),
+ SOORTimeout =
+ xml:get_path_s(El, [{elem, "session"}, {attr, "duration"}]),
+ Status = xml:get_path_s(El, [{elem, "status"}, cdata]),
+ Show = xml:get_path_s(El, [{elem, "status"}, {attr, "type"}]),
+ SSendBody = xml:get_path_s(El, [{elem, "body"}, {attr, "send"}]),
+ SendBody =
+ case SSendBody of
+ "all" -> all;
+ "first-per-user" -> first_per_user;
+ "first" -> first;
+ "none" -> none;
+ _ -> none
+ end,
+ SendGroupchat =
+ xml:get_path_s(El, [{elem, "body"},
+ {attr, "groupchat"}]) == "true",
+ SendFrom = send_from(El),
+ AppID = xml:get_path_s(El, [{elem, "appid"}, cdata]),
+ {Offline, Keep} =
+ case xml:get_path_s(El, [{elem, "offline"}, cdata]) of
+ "true" -> {true, false};
+ "keep" -> {false, true};
+ _ -> {false, false}
+ end,
+ Notification1 = xml:get_path_s(El, [{elem, "notification"}]),
+ Notification =
+ case Notification1 of
+ {xmlelement, _, _, _} ->
+ Notification1;
+ _ ->
+ {xmlelement, "notification", [],
+ [{xmlelement, "type", [],
+ [{xmlcdata, "none"}]}]}
+ end,
+ case catch {list_to_integer(SKeepAlive),
+ list_to_integer(SOORTimeout)} of
+ {KeepAlive, OORTimeout}
+ when OORTimeout =< ?MAX_OOR_TIMEOUT ->
+ if
+ Offline ->
+ ejabberd_hooks:run(
+ p1_push_enable_offline,
+ StateData#state.server,
+ [StateData#state.jid,
+ Notification, SendBody, SendFrom, AppID]);
+ Keep ->
+ ok;
+ true ->
+ ejabberd_hooks:run(
+ p1_push_disable,
+ StateData#state.server,
+ [StateData#state.jid,
+ Notification,
+ AppID])
+ end,
+ NSD1 =
+ StateData#state{keepalive_timeout = KeepAlive,
+ oor_timeout = OORTimeout * 60,
+ oor_status = Status,
+ oor_show = Show,
+ oor_notification = Notification,
+ oor_send_body = SendBody,
+ oor_send_groupchat = SendGroupchat,
+ oor_send_from = SendFrom,
+ oor_appid = AppID,
+ oor_offline = Offline},
+ NSD2 = start_keepalive_timer(NSD1),
+ {{result, []}, NSD2};
+ _ ->
+ {{error, ?ERR_BAD_REQUEST}, StateData}
+ end;
+ {xmlelement, "disable", _, _} ->
+ ejabberd_hooks:run(
+ p1_push_disable,
+ StateData#state.server,
+ [StateData#state.jid,
+ StateData#state.oor_notification,
+ StateData#state.oor_appid]),
+ NSD1 =
+ StateData#state{keepalive_timeout = undefined,
+ oor_timeout = undefined,
+ oor_status = "",
+ oor_show = "",
+ oor_notification = undefined,
+ oor_send_body = all},
+ NSD2 = start_keepalive_timer(NSD1),
+ {{result, []}, NSD2};
+ {xmlelement, "badge", _, _} ->
+ SBadge = xml:get_path_s(El, [{attr, "unread"}]),
+ Badge =
+ case catch list_to_integer(SBadge) of
+ B when is_integer(B) ->
+ B;
+ _ ->
+ 0
+ end,
+ NSD1 =
+ StateData#state{oor_unread_client = Badge},
+ {{result, []}, NSD1};
+ _ ->
+ {{error, ?ERR_BAD_REQUEST}, StateData}
+ end,
+ IQRes =
+ case Res of
+ {result, Result} ->
+ IQ#iq{type = result, sub_el = Result};
+ {error, Error} ->
+ IQ#iq{type = error, sub_el = [El, Error]}
+ end,
+ ejabberd_router:route(
+ To, From, jlib:iq_to_xml(IQRes)),
+ NewStateData.
+
+check_x_pushed({xmlelement, _Name, _Attrs, Els}) ->
+ check_x_pushed1(Els).
+
+check_x_pushed1([]) ->
+ false;
+check_x_pushed1([{xmlcdata, _} | Els]) ->
+ check_x_pushed1(Els);
+check_x_pushed1([El | Els]) ->
+ case xml:get_tag_attr_s("xmlns", El) of
+ ?NS_P1_PUSHED ->
+ true;
+ _ ->
+ check_x_pushed1(Els)
+ end.
+
+check_x_attachment({xmlelement, _Name, _Attrs, Els}) ->
+ check_x_attachment1(Els).
+
+check_x_attachment1([]) ->
+ false;
+check_x_attachment1([{xmlcdata, _} | Els]) ->
+ check_x_attachment1(Els);
+check_x_attachment1([El | Els]) ->
+ case xml:get_tag_attr_s("xmlns", El) of
+ ?NS_P1_ATTACHMENT ->
+ true;
+ _ ->
+ check_x_attachment1(Els)
+ end.
+
+%% TODO: Delete XEP-0091 stuff once it is Obsolete
+maybe_add_delay(El, TZ, From, Desc) ->
+ maybe_add_delay(El, TZ, From, Desc, calendar:now_to_universal_time(now())).
+maybe_add_delay({xmlelement, _, _, Els} = El, TZ, From, Desc, TimeStamp) ->
+ HasOldTS = lists:any(
+ fun({xmlelement, "x", Attrs, _}) ->
+ xml:get_attr_s("xmlns", Attrs) == ?NS_DELAY91;
+ (_) ->
+ false
+ end, Els),
+ HasNewTS = lists:any(
+ fun({xmlelement, "delay", Attrs, _}) ->
+ xml:get_attr_s("xmlns", Attrs) == ?NS_DELAY;
+ (_) ->
+ false
+ end, Els),
+ El1 = if not HasOldTS ->
+ xml:append_subtags(El, [jlib:timestamp_to_xml(TimeStamp)]);
+ true ->
+ El
+ end,
+ if not HasNewTS ->
+ xml:append_subtags(
+ El1, [jlib:timestamp_to_xml(TimeStamp, TZ, From, Desc)]);
+ true ->
+ El1
+ end.
+
+send_from(El) ->
+ %% First test previous version attribute:
+ case xml:get_path_s(El, [{elem, "body"}, {attr, "jid"}]) of
+ "false" ->
+ none;
+ "true" ->
+ jid;
+ "" ->
+ case xml:get_path_s(El, [{elem, "body"}, {attr, "from"}]) of
+ "jid" -> jid;
+ "username" -> username;
+ "name" -> name;
+ "none" -> none;
+ _ -> jid
+ end
+ end.
+
fsm_limit_opts(Opts) ->
case lists:keysearch(max_fsm_queue, 1, Opts) of
{value, {_, N}} when is_integer(N) ->
@@ -2383,3 +3557,69 @@ pack_string(String, Pack) ->
none ->
{String, gb_trees:insert(String, String, Pack)}
end.
+
+
+%% @spec () -> string()
+%% @doc Build the content of a Flash policy file.
+%% It specifies as domain "*".
+%% It specifies as to-ports the ports that serve ejabberd_c2s.
+flash_policy_string() ->
+ Listen = ejabberd_config:get_local_option(listen),
+ ClientPortsDeep = ["," ++ integer_to_list(Port)
+ || {{Port,_,_}, ejabberd_c2s, _Opts} <- Listen],
+ %% NOTE: The function string:join/2 was introduced in Erlang/OTP R12B-0
+ %% so it can't be used yet in ejabberd.
+ ToPortsString = case lists:flatten(ClientPortsDeep) of
+ [$, | Tail] -> Tail;
+ _ -> []
+ end,
+
+ "<?xml version=\"1.0\"?>\n"
+ "<!DOCTYPE cross-domain-policy SYSTEM "
+ "\"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd\">\n"
+ "<cross-domain-policy>\n"
+ " <allow-access-from domain=\"*\" to-ports=\""
+ ++ ToPortsString ++
+ "\"/>\n"
+ "</cross-domain-policy>\n\0".
+
+need_redirect(#state{redirect = true, user = User, server = Server}) ->
+ LUser = jlib:nodeprep(User),
+ LServer = jlib:nameprep(Server),
+ case ejabberd_cluster:get_node({LUser, LServer}) of
+ Node when node() == Node ->
+ false;
+ Node ->
+ case rpc:call(Node, ejabberd_config,
+ get_local_option, [hostname], 5000) of
+ Host when is_list(Host) ->
+ {true, Host};
+ _ ->
+ false
+ end
+ end;
+need_redirect(_) ->
+ false.
+
+get_jid_from_opts(Opts) ->
+ case lists:keysearch(jid, 1, Opts) of
+ {value, {_, JIDValue}} ->
+ JID = case JIDValue of
+ {_U, _S, _R} ->
+ jlib:make_jid(JIDValue);
+ _ when is_binary(JIDValue) ->
+ jlib:string_to_jid(binary_to_list(JIDValue));
+ _ when is_list(JIDValue) ->
+ jlib:string_to_jid(JIDValue);
+ _ ->
+ JIDValue
+ end,
+ {ok, JID};
+ _ ->
+ error
+ end.
+
+is_remote_receiver(#socket_state{receiver = Pid}) when is_pid(Pid) ->
+ node(Pid) /= node();
+is_remote_receiver(_) ->
+ false.
diff --git a/src/ejabberd_c2s.hrl b/src/ejabberd_c2s.hrl
new file mode 100644
index 000000000..7889c0de5
--- /dev/null
+++ b/src/ejabberd_c2s.hrl
@@ -0,0 +1,88 @@
+%%%----------------------------------------------------------------------
+%%%
+%%% ejabberd, Copyright (C) 2002-2010 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+
+-ifndef(mod_privacy_hrl).
+-include("mod_privacy.hrl").
+-endif.
+
+-define(SETS, gb_sets).
+-define(DICT, dict).
+
+%% pres_a contains all the presence available send (either through roster mechanism or directed).
+%% Directed presence unavailable remove user from pres_a.
+-record(state, {socket,
+ sockmod,
+ socket_monitor,
+ xml_socket,
+ streamid,
+ sasl_state,
+ access,
+ shaper,
+ zlib = false,
+ tls = false,
+ tls_required = false,
+ tls_enabled = false,
+ tls_options = [],
+ authenticated = false,
+ jid,
+ user = "", server = ?MYNAME, resource = "",
+ sid,
+ pres_t = ?SETS:new(),
+ pres_f = ?SETS:new(),
+ pres_a = ?SETS:new(),
+ pres_i = ?SETS:new(),
+ pres_last, pres_pri,
+ pres_timestamp,
+ pres_invis = false,
+ privacy_list = #userlist{},
+ conn = unknown,
+ auth_module = unknown,
+ ip,
+ redirect = false,
+ aux_fields = [],
+ fsm_limit_opts,
+ lang,
+ debug=false,
+ flash_connection = false,
+ reception = true,
+ standby = false,
+ queue = queue:new(),
+ queue_len = 0,
+ pres_queue = gb_trees:empty(),
+ keepalive_timer,
+ keepalive_timeout,
+ oor_timeout,
+ oor_status = "",
+ oor_show = "",
+ oor_notification,
+ oor_send_body = all,
+ oor_send_groupchat = false,
+ oor_send_from = jid,
+ oor_appid = "",
+ oor_unread = 0,
+ oor_unread_users = ?SETS:new(),
+ oor_unread_client = 0,
+ oor_offline = false,
+ ack_enabled = false,
+ ack_counter = 0,
+ ack_queue = queue:new(),
+ ack_timer}).
diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl
index 13351bbaf..56d62e09c 100644
--- a/src/ejabberd_captcha.erl
+++ b/src/ejabberd_captcha.erl
@@ -49,20 +49,12 @@
-define(CAPTCHA_TEXT(Lang), translate:translate(Lang, "Enter the text you see")).
-define(CAPTCHA_LIFETIME, 120000). % two minutes
+-define(RPC_TIMEOUT, 5000).
-define(LIMIT_PERIOD, 60*1000*1000). % one minute
-record(state, {limits = treap:empty()}).
-record(captcha, {id, pid, key, tref, args}).
--define(T(S),
- case catch mnesia:transaction(fun() -> S end) of
- {atomic, Res} ->
- Res;
- {_, Reason} ->
- ?ERROR_MSG("mnesia transaction failed: ~p", [Reason]),
- {error, Reason}
- end).
-
%%====================================================================
%% API
%%====================================================================
@@ -78,7 +70,7 @@ create_captcha(SID, From, To, Lang, Limiter, Args)
is_record(From, jid), is_record(To, jid) ->
case create_image(Limiter) of
{ok, Type, Key, Image} ->
- Id = randoms:get_string(),
+ Id = randoms:get_string() ++ "-" ++ ejabberd_cluster:node_id(),
B64Image = jlib:encode_base64(binary_to_list(Image)),
JID = jlib:jid_to_string(From),
CID = "sha1+" ++ sha:sha(Image) ++ "@bob.xmpp.org",
@@ -105,13 +97,9 @@ create_captcha(SID, From, To, Lang, Limiter, Args)
OOB = {xmlelement, "x", [{"xmlns", ?NS_OOB}],
[{xmlelement, "url", [], [{xmlcdata, get_url(Id)}]}]},
Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}),
- case ?T(mnesia:write(#captcha{id=Id, pid=self(), key=Key,
- tref=Tref, args=Args})) of
- ok ->
- {ok, Id, [Body, OOB, Captcha, Data]};
- Err ->
- {error, Err}
- end;
+ ets:insert(captcha, #captcha{id=Id, pid=self(), key=Key,
+ tref=Tref, args=Args}),
+ {ok, Id, [Body, OOB, Captcha, Data]};
Err ->
Err
end.
@@ -122,7 +110,7 @@ create_captcha_x(SID, To, Lang, Limiter, HeadEls) ->
create_captcha_x(SID, To, Lang, Limiter, HeadEls, TailEls) ->
case create_image(Limiter) of
{ok, Type, Key, Image} ->
- Id = randoms:get_string(),
+ Id = randoms:get_string() ++ "-" ++ ejabberd_cluster:node_id(),
B64Image = jlib:encode_base64(binary_to_list(Image)),
CID = "sha1+" ++ sha:sha(Image) ++ "@bob.xmpp.org",
Data = {xmlelement, "data",
@@ -155,12 +143,8 @@ create_captcha_x(SID, To, Lang, Limiter, HeadEls, TailEls) ->
[{xmlelement, "uri", [{"type", Type}],
[{xmlcdata, "cid:" ++ CID}]}]}]}] ++ TailEls},
Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}),
- case ?T(mnesia:write(#captcha{id=Id, key=Key, tref=Tref})) of
- ok ->
- {ok, [Captcha, Data]};
- Err ->
- {error, Err}
- end;
+ ets:insert(captcha, #captcha{id=Id, key=Key, tref=Tref}),
+ {ok, [Captcha, Data]};
Err ->
Err
end.
@@ -172,8 +156,8 @@ create_captcha_x(SID, To, Lang, Limiter, HeadEls, TailEls) ->
%% IdEl = xmlelement()
%% KeyEl = xmlelement()
build_captcha_html(Id, Lang) ->
- case mnesia:dirty_read(captcha, Id) of
- [#captcha{}] ->
+ case lookup_captcha(Id) of
+ {ok, _} ->
ImgEl = {xmlelement, "img", [{"src", get_url(Id ++ "/image")}], []},
TextEl = {xmlcdata, ?CAPTCHA_TEXT(Lang)},
IdEl = {xmlelement, "input", [{"type", "hidden"},
@@ -203,29 +187,25 @@ build_captcha_html(Id, Lang) ->
%% @spec (Id::string(), ProvidedKey::string()) -> captcha_valid | captcha_non_valid | captcha_not_found
check_captcha(Id, ProvidedKey) ->
- ?T(case mnesia:read(captcha, Id, write) of
- [#captcha{pid=Pid, args=Args, key=StoredKey, tref=Tref}] ->
- mnesia:delete({captcha, Id}),
- erlang:cancel_timer(Tref),
- if StoredKey == ProvidedKey ->
- if is_pid(Pid) ->
- Pid ! {captcha_succeed, Args};
- true ->
- ok
- end,
- captcha_valid;
- true ->
- if is_pid(Pid) ->
- Pid ! {captcha_failed, Args};
- true ->
- ok
- end,
- captcha_non_valid
- end;
- _ ->
- captcha_not_found
- end).
-
+ case string:tokens(Id, "-") of
+ [_, NodeID] ->
+ case ejabberd_cluster:get_node_by_id(NodeID) of
+ Node when Node == node() ->
+ do_check_captcha(Id, ProvidedKey);
+ Node ->
+ case catch rpc:call(Node, ?MODULE, check_captcha,
+ [Id, ProvidedKey], ?RPC_TIMEOUT) of
+ {'EXIT', _} ->
+ captcha_not_found;
+ {badrpc, _} ->
+ captcha_not_found;
+ Res ->
+ Res
+ end
+ end;
+ _ ->
+ captcha_not_found
+ end.
process_reply({xmlelement, _, _, _} = El) ->
case xml:get_subtag(El, "x") of
@@ -236,28 +216,14 @@ process_reply({xmlelement, _, _, _} = El) ->
case catch {proplists:get_value("challenge", Fields),
proplists:get_value("ocr", Fields)} of
{[Id|_], [OCR|_]} ->
- ?T(case mnesia:read(captcha, Id, write) of
- [#captcha{pid=Pid, args=Args, key=Key, tref=Tref}] ->
- mnesia:delete({captcha, Id}),
- erlang:cancel_timer(Tref),
- if OCR == Key ->
- if is_pid(Pid) ->
- Pid ! {captcha_succeed, Args};
- true ->
- ok
- end,
- ok;
- true ->
- if is_pid(Pid) ->
- Pid ! {captcha_failed, Args};
- true ->
- ok
- end,
- {error, bad_match}
- end;
- _ ->
- {error, not_found}
- end);
+ case check_captcha(Id, OCR) of
+ captcha_valid ->
+ ok;
+ captcha_non_valid ->
+ {error, bad_match};
+ captcha_not_found ->
+ {error, not_found}
+ end;
_ ->
{error, malformed}
end
@@ -279,8 +245,8 @@ process(_Handlers, #request{method='GET', lang=Lang, path=[_, Id]}) ->
process(_Handlers, #request{method='GET', path=[_, Id, "image"], ip = IP}) ->
{Addr, _Port} = IP,
- case mnesia:dirty_read(captcha, Id) of
- [#captcha{key=Key}] ->
+ case lookup_captcha(Id) of
+ {ok, #captcha{key=Key}} ->
case create_image(Addr, Key) of
{ok, Type, _, Img} ->
{200,
@@ -321,10 +287,8 @@ process(_Handlers, _Request) ->
%% gen_server callbacks
%%====================================================================
init([]) ->
- mnesia:create_table(captcha,
- [{ram_copies, [node()]},
- {attributes, record_info(fields, captcha)}]),
- mnesia:add_table_copy(captcha, node(), ram_copies),
+ mnesia:delete_table(captcha),
+ ets:new(captcha, [named_table, public, {keypos, #captcha.id}]),
check_captcha_setup(),
{ok, #state{}}.
@@ -350,17 +314,17 @@ handle_cast(_Msg, State) ->
handle_info({remove_id, Id}, State) ->
?DEBUG("captcha ~p timed out", [Id]),
- _ = ?T(case mnesia:read(captcha, Id, write) of
- [#captcha{args=Args, pid=Pid}] ->
- if is_pid(Pid) ->
- Pid ! {captcha_failed, Args};
- true ->
- ok
- end,
- mnesia:delete({captcha, Id});
- _ ->
- ok
- end),
+ case ets:lookup(captcha, Id) of
+ [#captcha{args=Args, pid=Pid}] ->
+ if is_pid(Pid) ->
+ Pid ! {captcha_failed, Args};
+ true ->
+ ok
+ end,
+ ets:delete(captcha, Id);
+ _ ->
+ ok
+ end,
{noreply, State};
handle_info(_Info, State) ->
@@ -573,6 +537,54 @@ check_captcha_setup() ->
ok
end.
+lookup_captcha(Id) ->
+ case string:tokens(Id, "-") of
+ [_, NodeID] ->
+ case ejabberd_cluster:get_node_by_id(NodeID) of
+ Node when Node == node() ->
+ case ets:lookup(captcha, Id) of
+ [C] ->
+ {ok, C};
+ _ ->
+ {error, enoent}
+ end;
+ Node ->
+ case catch rpc:call(Node, ets, lookup,
+ [captcha, Id], ?RPC_TIMEOUT) of
+ [C] ->
+ {ok, C};
+ _ ->
+ {error, enoent}
+ end
+ end;
+ _ ->
+ {error, enoent}
+ end.
+
+do_check_captcha(Id, ProvidedKey) ->
+ case ets:lookup(captcha, Id) of
+ [#captcha{pid = Pid, args = Args, key = ValidKey, tref = Tref}] ->
+ ets:delete(captcha, Id),
+ erlang:cancel_timer(Tref),
+ if ValidKey == ProvidedKey ->
+ if is_pid(Pid) ->
+ Pid ! {captcha_succeed, Args};
+ true ->
+ ok
+ end,
+ captcha_valid;
+ true ->
+ if is_pid(Pid) ->
+ Pid ! {captcha_failed, Args};
+ true ->
+ ok
+ end,
+ captcha_non_valid
+ end;
+ _ ->
+ captcha_not_found
+ end.
+
clean_treap(Treap, CleanPriority) ->
case treap:is_empty(Treap) of
true ->
diff --git a/src/ejabberd_cluster.erl b/src/ejabberd_cluster.erl
new file mode 100644
index 000000000..ec72b063e
--- /dev/null
+++ b/src/ejabberd_cluster.erl
@@ -0,0 +1,252 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_cluster.erl
+%%% Author : Evgeniy Khramtsov <ekhramtsov@process-one.net>
+%%% Description :
+%%%
+%%% Created : 2 Apr 2010 by Evgeniy Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(ejabberd_cluster).
+
+-behaviour(gen_server).
+
+%% API
+-export([start_link/0, get_node/1, get_node_new/1, announce/1, shutdown/0,
+ node_id/0, get_node_by_id/1, get_nodes/0, rehash_timeout/0, start/0,
+ shutdown_migrate/1, migrate_timeout/0]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-include("ejabberd.hrl").
+
+-define(HASHTBL, nodes_hash).
+-define(HASHTBL_NEW, nodes_hash_new).
+-define(POINTS, 64).
+-define(REHASH_TIMEOUT, timer:seconds(30)).
+-define(MIGRATE_TIMEOUT, timer:minutes(2)).
+-define(LOCK, {migrate, node()}).
+
+-record(state, {}).
+
+%%====================================================================
+%% API
+%%====================================================================
+start() ->
+ ChildSpec = {?MODULE,
+ {?MODULE, start_link, []},
+ permanent,
+ brutal_kill,
+ worker,
+ [?MODULE]},
+ supervisor:start_child(ejabberd_sup, ChildSpec).
+
+start_link() ->
+ gen_server:start_link(?MODULE, [], []).
+
+get_node(Key) ->
+ Hash = erlang:phash2(Key),
+ get_node_by_hash(?HASHTBL, Hash).
+
+get_node_new(Key) ->
+ Hash = erlang:phash2(Key),
+ get_node_by_hash(?HASHTBL_NEW, Hash).
+
+get_nodes() ->
+ %% TODO
+ mnesia:system_info(running_db_nodes).
+
+announce(Pid) ->
+ gen_server:call(Pid, announce, infinity).
+
+node_id() ->
+ integer_to_list(erlang:phash2(node())).
+
+rehash_timeout() ->
+ case ejabberd_config:get_local_option(rehash_timeout) of
+ N when is_integer(N), N > 0 ->
+ timer:seconds(N);
+ _ ->
+ ?REHASH_TIMEOUT
+ end.
+
+migrate_timeout() ->
+ case ejabberd_config:get_local_option(migrate_timeout) of
+ N when is_integer(N), N > 0 ->
+ timer:seconds(N);
+ _ ->
+ ?MIGRATE_TIMEOUT
+ end.
+
+get_node_by_id(NodeID) when is_list(NodeID) ->
+ case catch list_to_existing_atom(NodeID) of
+ {'EXIT', _} ->
+ node();
+ Res ->
+ get_node_by_id(Res)
+ end;
+get_node_by_id(NodeID) ->
+ case global:whereis_name(NodeID) of
+ Pid when is_pid(Pid) ->
+ node(Pid);
+ _ ->
+ node()
+ end.
+
+shutdown() ->
+ lists:foreach(
+ fun(Node) when Node /= node() ->
+ {ejabberd_cluster, Node} ! {node_down, node()};
+ (_) ->
+ ok
+ end, get_nodes()).
+
+shutdown_migrate(WaitTime) ->
+ delete_node(?HASHTBL_NEW, node()),
+ ejabberd_hooks:run(node_down, [node()]),
+ shutdown(),
+ delete_node(?HASHTBL, node()),
+ ejabberd_hooks:run(node_hash_update, [node(), down, WaitTime]),
+ ?INFO_MSG("Waiting ~p seconds for the migration to be completed.",
+ [WaitTime div 1000]),
+ timer:sleep(WaitTime),
+ ok.
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+init([]) ->
+ {A, B, C} = now(),
+ random:seed(A, B, C),
+ net_kernel:monitor_nodes(true, [{node_type, visible}]),
+ ets:new(?HASHTBL, [named_table, public, ordered_set]),
+ ets:new(?HASHTBL_NEW, [named_table, public, ordered_set]),
+ register_node(),
+ AllNodes = get_nodes(),
+ OtherNodes = case AllNodes of
+ [_MyNode] ->
+ AllNodes;
+ _ ->
+ AllNodes -- [node()]
+ end,
+ append_nodes(?HASHTBL, OtherNodes),
+ append_nodes(?HASHTBL_NEW, AllNodes),
+ {ok, #state{}}.
+
+handle_call(announce, _From, State) ->
+ Migrate_timeout = migrate_timeout(),
+ case global:set_lock(?LOCK, get_nodes(), 0) of
+ false ->
+ ?WARNING_MSG("Another node is recently attached to "
+ "the cluster and is being rebalanced. "
+ "Waiting for the rebalancing to be completed "
+ "before starting this node. "
+ "This will take at least ~p seconds. "
+ "Please, be patient.", [Migrate_timeout div 1000]),
+ global:set_lock(?LOCK, get_nodes(), infinity);
+ true ->
+ ok
+ end,
+ case get_nodes() of
+ [_MyNode] ->
+ register(?MODULE, self()),
+ global:del_lock(?LOCK);
+ Nodes ->
+ OtherNodes = Nodes -- [node()],
+ ?INFO_MSG("waiting for migration from nodes: ~w",
+ [OtherNodes]),
+ {_Res, BadNodes} = gen_server:multi_call(
+ OtherNodes, ?MODULE,
+ {node_ready, node()}, ?REHASH_TIMEOUT),
+ append_node(?HASHTBL, node()),
+ register(?MODULE, self()),
+ case OtherNodes -- BadNodes of
+ [] ->
+ global:del_lock(?LOCK);
+ WorkingNodes ->
+ gen_server:abcast(WorkingNodes, ?MODULE, {node_ready, node()}),
+ erlang:send_after(Migrate_timeout, self(), del_lock)
+ end
+ end,
+ {reply, ok, State};
+handle_call({node_ready, Node}, _From, State) ->
+ ?INFO_MSG("node ~p is ready, preparing migration", [Node]),
+ append_node(?HASHTBL_NEW, Node),
+ ejabberd_hooks:run(node_up, [Node]),
+ {reply, ok, State};
+handle_call(_Request, _From, State) ->
+ Reply = ok,
+ {reply, Reply, State}.
+
+handle_cast({node_ready, Node}, State) ->
+ ?INFO_MSG("adding node ~p to hash and starting migration", [Node]),
+ append_node(?HASHTBL, Node),
+ ejabberd_hooks:run(node_hash_update, [Node, up, migrate_timeout()]),
+ {noreply, State};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(del_lock, State) ->
+ global:del_lock(?LOCK),
+ {noreply, State};
+handle_info({node_down, Node}, State) ->
+ delete_node(?HASHTBL, Node),
+ delete_node(?HASHTBL_NEW, Node),
+ {noreply, State};
+handle_info({nodedown, Node, _}, State) ->
+ ?INFO_MSG("node ~p goes down", [Node]),
+ delete_node(?HASHTBL, Node),
+ delete_node(?HASHTBL_NEW, Node),
+ {noreply, State};
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Internal functions
+%%--------------------------------------------------------------------
+append_nodes(Tab, Nodes) ->
+ lists:foreach(
+ fun(Node) ->
+ append_node(Tab, Node)
+ end, Nodes).
+
+append_node(Tab, Node) ->
+ lists:foreach(
+ fun(I) ->
+ Hash = erlang:phash2({I, Node}),
+ ets:insert(Tab, {Hash, Node})
+ end, lists:seq(1, ?POINTS)).
+
+delete_node(Tab, Node) ->
+ lists:foreach(
+ fun(I) ->
+ Hash = erlang:phash2({I, Node}),
+ ets:delete(Tab, Hash)
+ end, lists:seq(1, ?POINTS)).
+
+get_node_by_hash(Tab, Hash) ->
+ NodeHash = case ets:next(Tab, Hash) of
+ '$end_of_table' ->
+ ets:first(Tab);
+ NH ->
+ NH
+ end,
+ if NodeHash == '$end_of_table' ->
+ erlang:error(no_running_nodes);
+ true ->
+ case ets:lookup(Tab, NodeHash) of
+ [] ->
+ get_node_by_hash(Tab, Hash);
+ [{_, Node}] ->
+ Node
+ end
+ end.
+
+register_node() ->
+ global:register_name(list_to_atom(node_id()), self()).
diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl
index f83d85671..709e74305 100644
--- a/src/ejabberd_config.erl
+++ b/src/ejabberd_config.erl
@@ -60,6 +60,13 @@ start() ->
load_file(Config),
%% This start time is used by mod_last:
add_local_option(node_start, now()),
+ SharedKey = case erlang:get_cookie() of
+ nocookie ->
+ sha:sha(randoms:get_string());
+ Cookie ->
+ sha:sha(atom_to_list(Cookie))
+ end,
+ add_local_option(shared_key, SharedKey),
ok.
%% @doc Get the filename of the ejabberd configuration file.
@@ -443,6 +450,12 @@ process_term(Term, State) ->
State;
{max_fsm_queue, N} ->
add_option(max_fsm_queue, N, State);
+ {hostname, Host} ->
+ add_option(hostname, Host, State);
+ {rehash_timeout, Secs} ->
+ add_option(rehash_timeout, Secs, State);
+ {migrate_timeout, Secs} ->
+ add_option(migrate_timeout, Secs, State);
{_Opt, _Val} ->
lists:foldl(fun(Host, S) -> process_host_term(Term, Host, S) end,
State, State#state.hosts)
diff --git a/src/ejabberd_frontend_socket.erl b/src/ejabberd_frontend_socket.erl
index 8e8a17fb0..9b4e44b07 100644
--- a/src/ejabberd_frontend_socket.erl
+++ b/src/ejabberd_frontend_socket.erl
@@ -45,6 +45,8 @@
get_peer_certificate/1,
get_verify_result/1,
close/1,
+ setopts/2,
+ change_controller/2,
sockname/1, peername/1]).
%% gen_server callbacks
@@ -94,18 +96,15 @@ start(Module, SockMod, Socket, Opts) ->
todo
end.
-starttls(FsmRef, _TLSOpts) ->
- %% TODO: Frontend improvements planned by Aleksey
- %%gen_server:call(FsmRef, {starttls, TLSOpts}),
- FsmRef.
+starttls(FsmRef, TLSOpts) ->
+ starttls(FsmRef, TLSOpts, undefined).
starttls(FsmRef, TLSOpts, Data) ->
gen_server:call(FsmRef, {starttls, TLSOpts, Data}),
FsmRef.
compress(FsmRef) ->
- gen_server:call(FsmRef, compress),
- FsmRef.
+ compress(FsmRef, undefined).
compress(FsmRef, Data) ->
gen_server:call(FsmRef, {compress, Data}),
@@ -138,11 +137,14 @@ close(FsmRef) ->
sockname(FsmRef) ->
gen_server:call(FsmRef, sockname).
-peername(_FsmRef) ->
- %% TODO: Frontend improvements planned by Aleksey
- %%gen_server:call(FsmRef, peername).
- {ok, {{0, 0, 0, 0}, 0}}.
+setopts(FsmRef, Opts) ->
+ gen_server:call(FsmRef, {setopts, Opts}).
+
+change_controller(FsmRef, C2SPid) ->
+ gen_server:call(FsmRef, {change_controller, C2SPid}).
+peername(FsmRef) ->
+ gen_server:call(FsmRef, peername).
%%====================================================================
%% gen_server callbacks
@@ -158,9 +160,16 @@ peername(_FsmRef) ->
init([Module, SockMod, Socket, Opts, Receiver]) ->
%% TODO: monitor the receiver
Node = ejabberd_node_groups:get_closest_node(backend),
+ IP = case peername(SockMod, Socket) of
+ {ok, IP1} ->
+ IP1;
+ _ ->
+ undefined
+ end,
{SockMod2, Socket2} = check_starttls(SockMod, Socket, Receiver, Opts),
{ok, Pid} =
- rpc:call(Node, Module, start, [{?MODULE, self()}, Opts]),
+ rpc:call(Node, Module, start,
+ [{?MODULE, self()}, [{frontend_ip, IP} | Opts]]),
ejabberd_receiver:become_controller(Receiver, Pid),
{ok, #state{sockmod = SockMod2,
socket = Socket2,
@@ -175,38 +184,16 @@ init([Module, SockMod, Socket, Opts, Receiver]) ->
%% {stop, Reason, State}
%% Description: Handling call messages
%%--------------------------------------------------------------------
-handle_call({starttls, TLSOpts}, _From, State) ->
- {ok, TLSSocket} = tls:tcp_to_tls(State#state.socket, TLSOpts),
- ejabberd_receiver:starttls(State#state.receiver, TLSSocket),
- Reply = ok,
- {reply, Reply, State#state{socket = TLSSocket, sockmod = tls},
- ?HIBERNATE_TIMEOUT};
-
handle_call({starttls, TLSOpts, Data}, _From, State) ->
- {ok, TLSSocket} = tls:tcp_to_tls(State#state.socket, TLSOpts),
- ejabberd_receiver:starttls(State#state.receiver, TLSSocket),
- catch (State#state.sockmod):send(
- State#state.socket, Data),
+ {ok, TLSSocket} = ejabberd_receiver:starttls(
+ State#state.receiver, TLSOpts, Data),
Reply = ok,
{reply, Reply, State#state{socket = TLSSocket, sockmod = tls},
?HIBERNATE_TIMEOUT};
-handle_call(compress, _From, State) ->
- {ok, ZlibSocket} = ejabberd_zlib:enable_zlib(
- State#state.sockmod,
- State#state.socket),
- ejabberd_receiver:compress(State#state.receiver, ZlibSocket),
- Reply = ok,
- {reply, Reply, State#state{socket = ZlibSocket, sockmod = ejabberd_zlib},
- ?HIBERNATE_TIMEOUT};
-
handle_call({compress, Data}, _From, State) ->
- {ok, ZlibSocket} = ejabberd_zlib:enable_zlib(
- State#state.sockmod,
- State#state.socket),
- ejabberd_receiver:compress(State#state.receiver, ZlibSocket),
- catch (State#state.sockmod):send(
- State#state.socket, Data),
+ {ok, ZlibSocket} = ejabberd_receiver:compress(
+ State#state.receiver, Data),
Reply = ok,
{reply, Reply, State#state{socket = ZlibSocket, sockmod = ejabberd_zlib},
?HIBERNATE_TIMEOUT};
@@ -246,13 +233,7 @@ handle_call(close, _From, State) ->
handle_call(sockname, _From, State) ->
#state{sockmod = SockMod, socket = Socket} = State,
- Reply =
- case SockMod of
- gen_tcp ->
- inet:sockname(Socket);
- _ ->
- SockMod:sockname(Socket)
- end,
+ Reply = peername(SockMod, Socket),
{reply, Reply, State, ?HIBERNATE_TIMEOUT};
handle_call(peername, _From, State) ->
@@ -266,6 +247,14 @@ handle_call(peername, _From, State) ->
end,
{reply, Reply, State, ?HIBERNATE_TIMEOUT};
+handle_call({setopts, Opts}, _From, State) ->
+ ejabberd_receiver:setopts(State#state.receiver, Opts),
+ {reply, ok, State, ?HIBERNATE_TIMEOUT};
+
+handle_call({change_controller, Pid}, _From, State) ->
+ ejabberd_receiver:change_controller(State#state.receiver, Pid),
+ {reply, ok, State, ?HIBERNATE_TIMEOUT};
+
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State, ?HIBERNATE_TIMEOUT}.
@@ -318,10 +307,16 @@ check_starttls(SockMod, Socket, Receiver, Opts) ->
end, Opts),
if
TLSEnabled ->
- {ok, TLSSocket} = tls:tcp_to_tls(Socket, TLSOpts),
- ejabberd_receiver:starttls(Receiver, TLSSocket),
+ {ok, TLSSocket} = ejabberd_receiver:starttls(Receiver, TLSOpts),
{tls, TLSSocket};
true ->
{SockMod, Socket}
end.
+peername(SockMod, Socket) ->
+ case SockMod of
+ gen_tcp ->
+ inet:peername(Socket);
+ _ ->
+ SockMod:peername(Socket)
+ end.
diff --git a/src/ejabberd_listener.erl b/src/ejabberd_listener.erl
index 967c23b17..6b1e526f5 100644
--- a/src/ejabberd_listener.erl
+++ b/src/ejabberd_listener.erl
@@ -35,7 +35,8 @@
stop_listener/2,
parse_listener_portip/2,
add_listener/3,
- delete_listener/2
+ delete_listener/2,
+ rate_limit/2
]).
-include("ejabberd.hrl").
@@ -274,6 +275,9 @@ get_ip_tuple(IPOpt, _IPVOpt) ->
IPOpt.
accept(ListenSocket, Module, Opts) ->
+ accept(ListenSocket, Module, Opts, 0).
+accept(ListenSocket, Module, Opts, Interval) ->
+ NewInterval = check_rate_limit(Interval),
case gen_tcp:accept(ListenSocket) of
{ok, Socket} ->
case {inet:sockname(Socket), inet:peername(Socket)} of
@@ -288,11 +292,11 @@ accept(ListenSocket, Module, Opts) ->
false -> ejabberd_socket
end,
CallMod:start(strip_frontend(Module), gen_tcp, Socket, Opts),
- accept(ListenSocket, Module, Opts);
+ accept(ListenSocket, Module, Opts, NewInterval);
{error, Reason} ->
?ERROR_MSG("(~w) Failed TCP accept: ~w",
[ListenSocket, Reason]),
- accept(ListenSocket, Module, Opts)
+ accept(ListenSocket, Module, Opts, NewInterval)
end.
udp_recv(Socket, Module, Opts) ->
@@ -520,3 +524,39 @@ format_error(Reason) ->
ReasonStr ->
ReasonStr
end.
+
+%% Set interval between two accepts on given port
+rate_limit([], _Interval) ->
+ ok;
+rate_limit([Port|Ports], Interval) ->
+ rate_limit(Port, Interval),
+ rate_limit(Ports, Interval);
+rate_limit(Port, Interval) ->
+ case get_listener_pid_by_port(Port) of
+ undefined -> no_listener;
+ Pid -> Pid ! {rate_limit, Interval}, ok
+ end.
+
+get_listener_pid_by_port(Port) ->
+ ListenerPids = [Pid || {{P,_,_},Pid,_,_} <-
+ supervisor:which_children(erlang:whereis(ejabberd_listeners)),
+ P == Port],
+ ListenerPid = case ListenerPids of
+ [] -> undefined;
+ [LPid|_] -> LPid
+ end,
+ ListenerPid.
+
+check_rate_limit(Interval) ->
+ NewInterval = receive
+ {rate_limit, AcceptInterval} ->
+ AcceptInterval
+ after 0 ->
+ Interval
+ end,
+ case NewInterval of
+ 0 -> ok;
+ Ms ->
+ timer:sleep(Ms)
+ end,
+ NewInterval.
diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl
index 13d946194..efe557e2c 100644
--- a/src/ejabberd_local.erl
+++ b/src/ejabberd_local.erl
@@ -130,7 +130,7 @@ route_iq(From, To, IQ, F) ->
route_iq(From, To, #iq{type = Type} = IQ, F, Timeout) when is_function(F) ->
Packet = if Type == set; Type == get ->
- ID = randoms:get_string(),
+ ID = ejabberd_router:make_id(),
Host = From#jid.lserver,
register_iq_response_handler(Host, ID, undefined, F, Timeout),
jlib:iq_to_xml(IQ#iq{id = ID});
@@ -150,10 +150,10 @@ register_iq_response_handler(_Host, ID, Module, Function, Timeout0) ->
N
end,
TRef = erlang:start_timer(Timeout, ejabberd_local, ID),
- mnesia:dirty_write(#iq_response{id = ID,
- module = Module,
- function = Function,
- timer = TRef}).
+ ets:insert(iq_response, #iq_response{id = ID,
+ module = Module,
+ function = Function,
+ timer = TRef}).
register_iq_handler(Host, XMLNS, Module, Fun) ->
ejabberd_local ! {register_iq_handler, Host, XMLNS, Module, Fun}.
@@ -195,11 +195,9 @@ init([]) ->
?MODULE, bounce_resource_packet, 100)
end, ?MYHOSTS),
catch ets:new(?IQTABLE, [named_table, public]),
- update_table(),
- mnesia:create_table(iq_response,
- [{ram_copies, [node()]},
- {attributes, record_info(fields, iq_response)}]),
- mnesia:add_table_copy(iq_response, node(), ram_copies),
+ mnesia:delete_table(iq_response),
+ catch ets:new(iq_response, [named_table, public,
+ {keypos, #iq_response.id}]),
{ok, #state{}}.
%%--------------------------------------------------------------------
@@ -271,7 +269,7 @@ handle_info(refresh_iq_handlers, State) ->
end, ets:tab2list(?IQTABLE)),
{noreply, State};
handle_info({timeout, _TRef, ID}, State) ->
- process_iq_timeout(ID),
+ spawn(fun() -> process_iq_timeout(ID) end),
{noreply, State};
handle_info(_Info, State) ->
{noreply, State}.
@@ -326,40 +324,22 @@ do_route(From, To, Packet) ->
end
end.
-update_table() ->
- case catch mnesia:table_info(iq_response, attributes) of
- [id, module, function] ->
- mnesia:delete_table(iq_response);
- [id, module, function, timer] ->
- ok;
- {'EXIT', _} ->
- ok
- end.
-
get_iq_callback(ID) ->
- case mnesia:dirty_read(iq_response, ID) of
+ case ets:lookup(iq_response, ID) of
[#iq_response{module = Module, timer = TRef,
function = Function}] ->
cancel_timer(TRef),
- mnesia:dirty_delete(iq_response, ID),
+ ets:delete(iq_response, ID),
{ok, Module, Function};
_ ->
error
end.
process_iq_timeout(ID) ->
- spawn(fun process_iq_timeout/0) ! ID.
-
-process_iq_timeout() ->
- receive
- ID ->
- case get_iq_callback(ID) of
- {ok, undefined, Function} ->
- Function(timeout);
- _ ->
- ok
- end
- after 5000 ->
+ case get_iq_callback(ID) of
+ {ok, undefined, Function} ->
+ Function(timeout);
+ _ ->
ok
end.
diff --git a/src/ejabberd_node_groups.erl b/src/ejabberd_node_groups.erl
index fc9307b46..d86ef8ce5 100644
--- a/src/ejabberd_node_groups.erl
+++ b/src/ejabberd_node_groups.erl
@@ -31,6 +31,7 @@
%% API
-export([start_link/0,
+ start/0,
join/1,
leave/1,
get_members/1,
@@ -40,14 +41,15 @@
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
+
+-record(state, {groups = []}).
+
-ifdef(SSL40).
-define(PG2, pg2).
-else.
-define(PG2, pg2_backport).
-endif.
--record(state, {}).
-
%%====================================================================
%% API
%%====================================================================
@@ -55,6 +57,15 @@
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
%% Description: Starts the server
%%--------------------------------------------------------------------
+start() ->
+ ChildSpec = {?MODULE,
+ {?MODULE, start_link, []},
+ permanent,
+ brutal_kill,
+ worker,
+ [?MODULE]},
+ supervisor:start_child(ejabberd_sup, ChildSpec).
+
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
@@ -87,30 +98,19 @@ get_closest_node(Name) ->
%% Description: Initiates the server
%%--------------------------------------------------------------------
init([]) ->
- {FE, BE} =
+ Groups =
case ejabberd_config:get_local_option(node_type) of
frontend ->
- {true, false};
+ [frontend];
backend ->
- {false, true};
+ [backend];
generic ->
- {true, true};
+ [frontend, backend];
undefined ->
- {true, true}
+ [frontend, backend]
end,
- if
- FE ->
- join(frontend);
- true ->
- ok
- end,
- if
- BE ->
- join(backend);
- true ->
- ok
- end,
- {ok, #state{}}.
+ lists:foreach(fun join/1, Groups),
+ {ok, #state{groups = Groups}}.
%%--------------------------------------------------------------------
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
@@ -150,7 +150,8 @@ handle_info(_Info, State) ->
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
-terminate(_Reason, _State) ->
+terminate(_Reason, #state{groups = Groups}) ->
+ lists:foreach(fun leave/1, Groups),
ok.
%%--------------------------------------------------------------------
diff --git a/src/ejabberd_receiver.erl b/src/ejabberd_receiver.erl
index 2c58ad51c..c26a9cd70 100644
--- a/src/ejabberd_receiver.erl
+++ b/src/ejabberd_receiver.erl
@@ -36,8 +36,12 @@
change_shaper/2,
reset_stream/1,
starttls/2,
+ starttls/3,
compress/2,
+ send/2,
become_controller/2,
+ change_controller/2,
+ setopts/2,
close/1]).
%% gen_server callbacks
@@ -52,6 +56,7 @@
c2s_pid,
max_stanza_size,
xml_stream_state,
+ tref,
timeout}).
-define(HIBERNATE_TIMEOUT, 90000).
@@ -86,15 +91,42 @@ change_shaper(Pid, Shaper) ->
reset_stream(Pid) ->
do_call(Pid, reset_stream).
-starttls(Pid, TLSSocket) ->
- do_call(Pid, {starttls, TLSSocket}).
+starttls(Pid, TLSOpts) ->
+ starttls(Pid, TLSOpts, undefined).
-compress(Pid, ZlibSocket) ->
- do_call(Pid, {compress, ZlibSocket}).
+starttls(Pid, TLSOpts, Data) ->
+ do_call(Pid, {starttls, TLSOpts, Data}).
+
+compress(Pid, Data) ->
+ do_call(Pid, {compress, Data}).
become_controller(Pid, C2SPid) ->
do_call(Pid, {become_controller, C2SPid}).
+change_controller(Pid, C2SPid) ->
+ case catch gen_server:call(Pid, {change_controller, C2SPid}) of
+ {'EXIT', _} ->
+ {error, einval};
+ Res ->
+ Res
+ end.
+
+setopts(Pid, Opts) ->
+ case lists:member({active, false}, Opts) of
+ true ->
+ case catch gen_server:call(Pid, deactivate_socket) of
+ {'EXIT', _} ->
+ {error, einval};
+ Res ->
+ Res
+ end;
+ false ->
+ ok
+ end.
+
+send(Pid, Data) ->
+ gen_server:call(Pid, {send, Data}).
+
close(Pid) ->
gen_server:cast(Pid, close).
@@ -132,10 +164,17 @@ init([Socket, SockMod, Shaper, MaxStanzaSize]) ->
%% {stop, Reason, State}
%% Description: Handling call messages
%%--------------------------------------------------------------------
-handle_call({starttls, TLSSocket}, _From,
+handle_call({starttls, TLSOpts, Data}, _From,
#state{xml_stream_state = XMLStreamState,
c2s_pid = C2SPid,
+ socket = Socket,
max_stanza_size = MaxStanzaSize} = State) ->
+ {ok, TLSSocket} = tls:tcp_to_tls(Socket, TLSOpts),
+ if Data /= undefined ->
+ do_send(State, Data);
+ true ->
+ ok
+ end,
close_stream(XMLStreamState),
NewXMLStreamState = xml_stream:new(C2SPid, MaxStanzaSize),
NewState = State#state{socket = TLSSocket,
@@ -143,14 +182,23 @@ handle_call({starttls, TLSSocket}, _From,
xml_stream_state = NewXMLStreamState},
case tls:recv_data(TLSSocket, "") of
{ok, TLSData} ->
- {reply, ok, process_data(TLSData, NewState), ?HIBERNATE_TIMEOUT};
+ {reply, {ok, TLSSocket},
+ process_data(TLSData, NewState), ?HIBERNATE_TIMEOUT};
{error, _Reason} ->
{stop, normal, ok, NewState}
end;
-handle_call({compress, ZlibSocket}, _From,
+handle_call({compress, Data}, _From,
#state{xml_stream_state = XMLStreamState,
c2s_pid = C2SPid,
+ socket = Socket,
+ sock_mod = SockMod,
max_stanza_size = MaxStanzaSize} = State) ->
+ {ok, ZlibSocket} = ejabberd_zlib:enable_zlib(SockMod, Socket),
+ if Data /= undefined ->
+ do_send(State, Data);
+ true ->
+ ok
+ end,
close_stream(XMLStreamState),
NewXMLStreamState = xml_stream:new(C2SPid, MaxStanzaSize),
NewState = State#state{socket = ZlibSocket,
@@ -158,7 +206,8 @@ handle_call({compress, ZlibSocket}, _From,
xml_stream_state = NewXMLStreamState},
case ejabberd_zlib:recv_data(ZlibSocket, "") of
{ok, ZlibData} ->
- {reply, ok, process_data(ZlibData, NewState), ?HIBERNATE_TIMEOUT};
+ {reply, {ok, ZlibSocket},
+ process_data(ZlibData, NewState), ?HIBERNATE_TIMEOUT};
{error, _Reason} ->
{stop, normal, ok, NewState}
end;
@@ -172,12 +221,31 @@ handle_call(reset_stream, _From,
{reply, Reply, State#state{xml_stream_state = NewXMLStreamState},
?HIBERNATE_TIMEOUT};
handle_call({become_controller, C2SPid}, _From, State) ->
+ erlang:monitor(process, C2SPid),
XMLStreamState = xml_stream:new(C2SPid, State#state.max_stanza_size),
NewState = State#state{c2s_pid = C2SPid,
xml_stream_state = XMLStreamState},
activate_socket(NewState),
Reply = ok,
{reply, Reply, NewState, ?HIBERNATE_TIMEOUT};
+handle_call({change_controller, C2SPid}, _From, State) ->
+ erlang:monitor(process, C2SPid),
+ NewXMLStreamState = xml_stream:change_callback_pid(
+ State#state.xml_stream_state, C2SPid),
+ NewState = State#state{c2s_pid = C2SPid,
+ xml_stream_state = NewXMLStreamState},
+ activate_socket(NewState),
+ {reply, ok, NewState, ?HIBERNATE_TIMEOUT};
+handle_call({send, Data}, _From, State) ->
+ case do_send(State, Data) of
+ ok ->
+ {reply, ok, State, ?HIBERNATE_TIMEOUT};
+ {error, _Reason} = Err ->
+ {stop, normal, Err, State}
+ end;
+handle_call(deactivate_socket, _From, State) ->
+ deactivate_socket(State),
+ {reply, ok, State, ?HIBERNATE_TIMEOUT};
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State, ?HIBERNATE_TIMEOUT}.
@@ -237,6 +305,9 @@ handle_info({Tag, _TCPSocket, Reason}, State)
_ ->
{stop, normal, State}
end;
+handle_info({'DOWN', _MRef, process, C2SPid, _},
+ #state{c2s_pid = C2SPid} = State) ->
+ {stop, normal, State};
handle_info({timeout, _Ref, activate}, State) ->
activate_socket(State),
{noreply, State, ?HIBERNATE_TIMEOUT};
@@ -294,6 +365,17 @@ activate_socket(#state{socket = Socket,
ok
end.
+deactivate_socket(#state{socket = Socket,
+ tref = TRef,
+ sock_mod = SockMod}) ->
+ cancel_timer(TRef),
+ case SockMod of
+ gen_tcp ->
+ inet:setopts(Socket, [{active, false}]);
+ _ ->
+ SockMod:setopts(Socket, [{active, false}])
+ end.
+
%% Data processing for connectors directly generating xmlelement in
%% Erlang data structure.
%% WARNING: Shaper does not work with Erlang data structure.
@@ -315,20 +397,23 @@ process_data([Element|Els], #state{c2s_pid = C2SPid} = State)
%% Data processing for connectors receivind data as string.
process_data(Data,
#state{xml_stream_state = XMLStreamState,
+ tref = TRef,
shaper_state = ShaperState,
c2s_pid = C2SPid} = State) ->
?DEBUG("Received XML on stream = ~p", [binary_to_list(Data)]),
XMLStreamState1 = xml_stream:parse(XMLStreamState, Data),
{NewShaperState, Pause} = shaper:update(ShaperState, size(Data)),
- if
- C2SPid == undefined ->
- ok;
- Pause > 0 ->
- erlang:start_timer(Pause, self(), activate);
- true ->
- activate_socket(State)
- end,
+ NewTRef = if
+ C2SPid == undefined ->
+ TRef;
+ Pause > 0 ->
+ erlang:start_timer(Pause, self(), activate);
+ true ->
+ activate_socket(State),
+ TRef
+ end,
State#state{xml_stream_state = XMLStreamState1,
+ tref = NewTRef,
shaper_state = NewShaperState}.
%% Element coming from XML parser are wrapped inside xmlstreamelement
@@ -346,6 +431,24 @@ close_stream(undefined) ->
close_stream(XMLStreamState) ->
xml_stream:close(XMLStreamState).
+do_send(State, Data) ->
+ (State#state.sock_mod):send(State#state.socket, Data).
+
+cancel_timer(TRef) when is_reference(TRef) ->
+ case erlang:cancel_timer(TRef) of
+ false ->
+ receive
+ {timeout, TRef, _} ->
+ ok
+ after 0 ->
+ ok
+ end;
+ _ ->
+ ok
+ end;
+cancel_timer(_) ->
+ ok.
+
do_call(Pid, Msg) ->
case catch gen_server:call(Pid, Msg) of
{'EXIT', Why} ->
diff --git a/src/ejabberd_router.erl b/src/ejabberd_router.erl
index 1e38dba29..8aa337c4c 100644
--- a/src/ejabberd_router.erl
+++ b/src/ejabberd_router.erl
@@ -38,7 +38,8 @@
unregister_route/1,
unregister_routes/1,
dirty_get_all_routes/0,
- dirty_get_all_domains/0
+ dirty_get_all_domains/0,
+ make_id/0
]).
-export([start_link/0]).
@@ -53,6 +54,9 @@
-record(route, {domain, pid, local_hint}).
-record(state, {}).
+%% "rr" stands for Record-Route.
+-define(ROUTE_PREFIX, "rr-").
+
%%====================================================================
%% API
%%====================================================================
@@ -65,7 +69,7 @@ start_link() ->
route(From, To, Packet) ->
- case catch do_route(From, To, Packet) of
+ case catch route_check_id(From, To, Packet) of
{'EXIT', Reason} ->
?ERROR_MSG("~p~nwhen processing: ~p",
[Reason, {From, To, Packet}]);
@@ -192,6 +196,8 @@ dirty_get_all_routes() ->
dirty_get_all_domains() ->
lists:usort(mnesia:dirty_all_keys(route)).
+make_id() ->
+ ?ROUTE_PREFIX ++ randoms:get_string() ++ "-" ++ ejabberd_cluster:node_id().
%%====================================================================
%% gen_server callbacks
@@ -309,6 +315,32 @@ code_change(_OldVsn, State, _Extra) ->
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
+route_check_id(From, To, {xmlelement, "iq", Attrs, _} = Packet) ->
+ case xml:get_attr_s("id", Attrs) of
+ ?ROUTE_PREFIX ++ Rest ->
+ Type = xml:get_attr_s("type", Attrs),
+ if Type == "error"; Type == "result" ->
+ case string:tokens(Rest, "-") of
+ [_, NodeID] ->
+ case ejabberd_cluster:get_node_by_id(NodeID) of
+ Node when Node == node() ->
+ do_route(From, To, Packet);
+ Node ->
+ {ejabberd_router, Node} !
+ {route, From, To, Packet}
+ end;
+ _ ->
+ do_route(From, To, Packet)
+ end;
+ true ->
+ do_route(From, To, Packet)
+ end;
+ _ ->
+ do_route(From, To, Packet)
+ end;
+route_check_id(From, To, Packet) ->
+ do_route(From, To, Packet).
+
do_route(OrigFrom, OrigTo, OrigPacket) ->
?DEBUG("route~n\tfrom ~p~n\tto ~p~n\tpacket ~p~n",
[OrigFrom, OrigTo, OrigPacket]),
@@ -346,9 +378,21 @@ do_route(OrigFrom, OrigTo, OrigPacket) ->
jlib:jid_tolower(From));
bare_destination ->
jlib:jid_remove_resource(
- jlib:jid_tolower(To))
+ jlib:jid_tolower(To));
+ broadcast ->
+ broadcast
end,
case get_component_number(LDstDomain) of
+ _ when Value == broadcast ->
+ lists:foreach(
+ fun(R) ->
+ Pid = R#route.pid,
+ if is_pid(Pid) ->
+ Pid ! {route, From, To, Packet};
+ true ->
+ drop
+ end
+ end, Rs);
undefined ->
case [R || R <- Rs, node(R#route.pid) == node()] of
[] ->
@@ -383,6 +427,7 @@ do_route(OrigFrom, OrigTo, OrigPacket) ->
end
end;
drop ->
+ ?DEBUG("packet dropped~n", []),
ok
end.
@@ -413,4 +458,3 @@ update_tables() ->
false ->
ok
end.
-
diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl
index 92e7d8bef..d684400d7 100644
--- a/src/ejabberd_s2s.erl
+++ b/src/ejabberd_s2s.erl
@@ -129,7 +129,7 @@ remove_connection(FromTo, Pid, Key) ->
end.
have_connection(FromTo) ->
- case catch mnesia:dirty_read(s2s, FromTo) of
+ case mnesia:dirty_read(s2s, FromTo) of
[_] ->
true;
_ ->
@@ -137,26 +137,37 @@ have_connection(FromTo) ->
end.
has_key(FromTo, Key) ->
- case mnesia:dirty_select(s2s,
- [{#s2s{fromto = FromTo, key = Key, _ = '_'},
- [],
- ['$_']}]) of
- [] ->
- false;
- _ ->
- true
+ Query = [{#s2s{fromto = FromTo, key = Key, _ = '_'},
+ [],
+ ['$_']}],
+ case get_node_by_key(Key) of
+ Node when Node == node() ->
+ case mnesia:dirty_select(s2s, Query) of
+ [] ->
+ false;
+ _ ->
+ true
+ end;
+ Node ->
+ case catch rpc:call(Node, mnesia, dirty_select,
+ [s2s, Query], 5000) of
+ [_|_] ->
+ true;
+ _ ->
+ false
+ end
end.
get_connections_pids(FromTo) ->
case catch mnesia:dirty_read(s2s, FromTo) of
- L when is_list(L) ->
- [Connection#s2s.pid || Connection <- L];
- _ ->
- []
+ L when is_list(L) ->
+ [Connection#s2s.pid || Connection <- L];
+ _ ->
+ []
end.
try_register(FromTo) ->
- Key = randoms:get_string(),
+ Key = new_key(),
MaxS2SConnectionsNumber = max_s2s_connections_number(FromTo),
MaxS2SConnectionsNumberPerNode =
max_s2s_connections_number_per_node(FromTo),
@@ -183,7 +194,17 @@ try_register(FromTo) ->
end.
dirty_get_connections() ->
- mnesia:dirty_all_keys(s2s).
+ lists:flatmap(
+ fun(Node) when Node == node() ->
+ mnesia:dirty_all_keys(s2s);
+ (Node) ->
+ case catch rpc:call(Node, mnesia, dirty_all_keys, [s2s], 5000) of
+ L when is_list(L) ->
+ L;
+ _ ->
+ []
+ end
+ end, ejabberd_cluster:get_nodes()).
%%====================================================================
%% gen_server callbacks
@@ -198,10 +219,10 @@ dirty_get_connections() ->
%%--------------------------------------------------------------------
init([]) ->
update_tables(),
- mnesia:create_table(s2s, [{ram_copies, [node()]}, {type, bag},
+ mnesia:create_table(s2s, [{ram_copies, [node()]},
+ {type, bag}, {local_content, true},
{attributes, record_info(fields, s2s)}]),
mnesia:add_table_copy(s2s, node(), ram_copies),
- mnesia:subscribe(system),
ejabberd_commands:register_commands(commands()),
mnesia:create_table(temporarily_blocked, [{ram_copies, [node()]}, {attributes, record_info(fields, temporarily_blocked)}]),
{ok, #state{}}.
@@ -234,9 +255,6 @@ handle_cast(_Msg, State) ->
%% {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
-handle_info({mnesia_system_event, {mnesia_down, Node}}, State) ->
- clean_table_from_bad_node(Node),
- {noreply, State};
handle_info({route, From, To, Packet}, State) ->
case catch do_route(From, To, Packet) of
{'EXIT', Reason} ->
@@ -270,19 +288,6 @@ code_change(_OldVsn, State, _Extra) ->
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
-clean_table_from_bad_node(Node) ->
- F = fun() ->
- Es = mnesia:select(
- s2s,
- [{#s2s{pid = '$1', _ = '_'},
- [{'==', {node, '$1'}, Node}],
- ['$_']}]),
- lists:foreach(fun(E) ->
- mnesia:delete_object(E)
- end, Es)
- end,
- mnesia:async_dirty(F).
-
do_route(From, To, Packet) ->
?DEBUG("s2s manager~n\tfrom ~p~n\tto ~p~n\tpacket ~P~n",
[From, To, Packet, 8]),
@@ -388,7 +393,7 @@ open_several_connections(N, MyServer, Server, From, FromTo,
new_connection(MyServer, Server, From, FromTo,
MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode) ->
- Key = randoms:get_string(),
+ Key = new_key(),
{ok, Pid} = ejabberd_s2s_out:start(
MyServer, Server, {new, Key}),
F = fun() ->
@@ -436,6 +441,20 @@ needed_connections_number(Ls, MaxS2SConnectionsNumber,
lists:min([MaxS2SConnectionsNumber - length(Ls),
MaxS2SConnectionsNumberPerNode - length(LocalLs)]).
+%%%-------------------------------------------------------------------
+%%% Dialback keys stuff
+%%%-------------------------------------------------------------------
+new_key() ->
+ randoms:get_string() ++ "-" ++ ejabberd_cluster:node_id().
+
+get_node_by_key(Key) ->
+ case string:tokens(Key, "-") of
+ [_, NodeID] ->
+ ejabberd_cluster:get_node_by_id(NodeID);
+ _ ->
+ node()
+ end.
+
%%--------------------------------------------------------------------
%% Function: is_service(From, To) -> true | false
%% Description: Return true if the destination must be considered as a
@@ -517,6 +536,12 @@ update_tables() ->
mnesia:delete_table(local_s2s);
false ->
ok
+ end,
+ case catch mnesia:table_info(s2s, local_content) of
+ false ->
+ mnesia:delete_table(s2s);
+ _ ->
+ ok
end.
%% Check if host is in blacklist or white list
diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl
index 65711fad4..d82add947 100644
--- a/src/ejabberd_s2s_out.erl
+++ b/src/ejabberd_s2s_out.erl
@@ -34,7 +34,8 @@
start_link/3,
start_connection/1,
terminate_if_waiting_delay/2,
- stop_connection/1]).
+ stop_connection/1,
+ stop_connection/2]).
%% p1_fsm callbacks (same as gen_fsm)
-export([init/1,
@@ -52,9 +53,9 @@
handle_sync_event/4,
handle_info/3,
terminate/3,
- print_state/1,
code_change/4,
test_get_addr_port/1,
+ print_state/1,
get_addr_port/1]).
-include("ejabberd.hrl").
@@ -86,10 +87,11 @@
%% Module start with or without supervisor:
-ifdef(NO_TRANSIENT_SUPERVISORS).
--define(SUPERVISOR_START, p1_fsm:start(ejabberd_s2s_out, [From, Host, Type],
- fsm_limit_opts() ++ ?FSMOPTS)).
+-define(SUPERVISOR_START, rpc:call(Node, p1_fsm, start,
+ [ejabberd_s2s_out, [From, Host, Type],
+ fsm_limit_opts() ++ ?FSMOPTS])).
-else.
--define(SUPERVISOR_START, supervisor:start_child(ejabberd_s2s_out_sup,
+-define(SUPERVISOR_START, supervisor:start_child({ejabberd_s2s_out_sup, Node},
[From, Host, Type])).
-endif.
@@ -129,6 +131,7 @@
%%% API
%%%----------------------------------------------------------------------
start(From, Host, Type) ->
+ Node = node(),
?SUPERVISOR_START.
start_link(From, Host, Type) ->
@@ -141,6 +144,9 @@ start_connection(Pid) ->
stop_connection(Pid) ->
p1_fsm:send_event(Pid, closed).
+stop_connection(Pid, Timeout) ->
+ p1_fsm:send_all_state_event(Pid, {closed, Timeout}).
+
%%%----------------------------------------------------------------------
%%% Callback functions from p1_fsm
%%%----------------------------------------------------------------------
@@ -783,6 +789,9 @@ stream_established(closed, StateData) ->
%% {next_state, NextStateName, NextStateData, Timeout} |
%% {stop, Reason, NewStateData}
%%----------------------------------------------------------------------
+handle_event({closed, Timeout}, StateName, StateData) ->
+ p1_fsm:send_event_after(Timeout, closed),
+ {next_state, StateName, StateData};
handle_event(_Event, StateName, StateData) ->
{next_state, StateName, StateData, get_timeout_interval(StateName)}.
diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl
index 860304d2d..fae566348 100644
--- a/src/ejabberd_sm.erl
+++ b/src/ejabberd_sm.erl
@@ -32,10 +32,16 @@
%% API
-export([start_link/0,
route/3,
- open_session/5, close_session/4,
+ set_session/6,
+ open_session/5,
+ open_session/6,
+ close_session/4,
+ close_migrated_session/4,
+ drop_session/1,
check_in_subscription/6,
bounce_offline_message/3,
disconnect_removed_user/2,
+ get_user_sessions/2,
get_user_resources/2,
set_presence/7,
unset_presence/6,
@@ -43,6 +49,7 @@
dirty_get_sessions_list/0,
dirty_get_my_sessions_list/0,
get_vh_session_list/1,
+ get_vh_my_session_list/1,
get_vh_session_number/1,
register_iq_handler/4,
register_iq_handler/5,
@@ -54,7 +61,10 @@
get_session_pid/3,
get_user_info/3,
get_user_ip/3,
- is_existing_resource/3
+ is_existing_resource/3,
+ node_up/1,
+ node_down/1,
+ migrate/3
]).
%% gen_server callbacks
@@ -67,7 +77,6 @@
-include("mod_privacy.hrl").
-record(session, {sid, usr, us, priority, info}).
--record(session_counter, {vhost, count}).
-record(state, {}).
%% default value for the maximum number of user connections
@@ -93,29 +102,48 @@ route(From, To, Packet) ->
end.
open_session(SID, User, Server, Resource, Info) ->
- set_session(SID, User, Server, Resource, undefined, Info),
- mnesia:dirty_update_counter(session_counter,
- jlib:nameprep(Server), 1),
+ open_session(SID, User, Server, Resource, undefined, Info).
+
+open_session(SID, User, Server, Resource, Priority, Info) ->
+ set_session(SID, User, Server, Resource, Priority, Info),
check_for_sessions_to_replace(User, Server, Resource),
JID = jlib:make_jid(User, Server, Resource),
ejabberd_hooks:run(sm_register_connection_hook, JID#jid.lserver,
[SID, JID, Info]).
close_session(SID, User, Server, Resource) ->
- Info = case mnesia:dirty_read({session, SID}) of
- [] -> [];
- [#session{info=I}] -> I
+ Info = do_close_session(SID),
+ US = {jlib:nodeprep(User), jlib:nameprep(Server)},
+ case ejabberd_cluster:get_node_new(US) of
+ Node when Node /= node() ->
+ rpc:cast(Node, ?MODULE, drop_session, [SID]);
+ _ ->
+ ok
end,
- F = fun() ->
- mnesia:delete({session, SID}),
- mnesia:dirty_update_counter(session_counter,
- jlib:nameprep(Server), -1)
- end,
- mnesia:sync_dirty(F),
JID = jlib:make_jid(User, Server, Resource),
ejabberd_hooks:run(sm_remove_connection_hook, JID#jid.lserver,
[SID, JID, Info]).
+close_migrated_session(SID, User, Server, Resource) ->
+ Info = do_close_session(SID),
+ JID = jlib:make_jid(User, Server, Resource),
+ ejabberd_hooks:run(sm_remove_migrated_connection_hook, JID#jid.lserver,
+ [SID, JID, Info]).
+
+do_close_session(SID) ->
+ Info = case mnesia:dirty_read({session, SID}) of
+ [] -> [];
+ [#session{info=I}] -> I
+ end,
+ drop_session(SID),
+ Info.
+
+drop_session(SID) ->
+ F = fun() ->
+ mnesia:delete({session, SID})
+ end,
+ mnesia:sync_dirty(F).
+
check_in_subscription(Acc, User, Server, _JID, _Type, _Reason) ->
case ejabberd_auth:is_user_exists(User, Server) of
true ->
@@ -135,15 +163,33 @@ disconnect_removed_user(User, Server) ->
{xmlelement, "broadcast", [],
[{exit, "User removed"}]}).
+get_user_sessions(User, Server) ->
+ LUser = jlib:nodeprep(User),
+ LServer = jlib:nameprep(Server),
+ US = {LUser, LServer},
+ case ejabberd_cluster:get_node({LUser, LServer}) of
+ Node when Node == node() ->
+ catch mnesia:dirty_index_read(session, US, #session.us);
+ Node ->
+ catch rpc:call(Node, mnesia, dirty_index_read,
+ [session, US, #session.us], 5000)
+ end.
+
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)]
+ Ss = case ejabberd_cluster:get_node({LUser, LServer}) of
+ Node when Node == node() ->
+ catch mnesia:dirty_index_read(session, US, #session.us);
+ Node ->
+ catch rpc:call(Node, mnesia, dirty_index_read,
+ [session, US, #session.us], 5000)
+ end,
+ if is_list(Ss) ->
+ [element(3, S#session.usr) || S <- clean_session_list(Ss)];
+ true ->
+ []
end.
get_user_ip(User, Server, Resource) ->
@@ -151,12 +197,18 @@ get_user_ip(User, Server, Resource) ->
LServer = jlib:nameprep(Server),
LResource = jlib:resourceprep(Resource),
USR = {LUser, LServer, LResource},
- case mnesia:dirty_index_read(session, USR, #session.usr) of
- [] ->
- undefined;
- Ss ->
+ Ss = case ejabberd_cluster:get_node({LUser, LServer}) of
+ Node when Node == node() ->
+ mnesia:dirty_index_read(session, USR, #session.usr);
+ Node ->
+ catch rpc:call(Node, mnesia, dirty_index_read,
+ [session, USR, #session.usr], 5000)
+ end,
+ if is_list(Ss), Ss /= [] ->
Session = lists:max(Ss),
- proplists:get_value(ip, Session#session.info)
+ proplists:get_value(ip, Session#session.info);
+ true ->
+ undefined
end.
get_user_info(User, Server, Resource) ->
@@ -164,15 +216,21 @@ get_user_info(User, Server, Resource) ->
LServer = jlib:nameprep(Server),
LResource = jlib:resourceprep(Resource),
USR = {LUser, LServer, LResource},
- case mnesia:dirty_index_read(session, USR, #session.usr) of
- [] ->
- offline;
- Ss ->
+ Ss = case ejabberd_cluster:get_node({LUser, LServer}) of
+ Node when Node == node() ->
+ mnesia:dirty_index_read(session, USR, #session.usr);
+ Node ->
+ catch rpc:call(Node, mnesia, dirty_index_read,
+ [session, USR, #session.usr], 5000)
+ end,
+ if is_list(Ss), Ss /= [] ->
Session = lists:max(Ss),
- Node = node(element(2, Session#session.sid)),
+ N = node(element(2, Session#session.sid)),
Conn = proplists:get_value(conn, Session#session.info),
IP = proplists:get_value(ip, Session#session.info),
- [{node, Node}, {conn, Conn}, {ip, IP}]
+ [{node, N}, {conn, Conn}, {ip, IP}];
+ true ->
+ offline
end.
set_presence(SID, User, Server, Resource, Priority, Presence, Info) ->
@@ -195,26 +253,37 @@ get_session_pid(User, Server, Resource) ->
LServer = jlib:nameprep(Server),
LResource = jlib:resourceprep(Resource),
USR = {LUser, LServer, LResource},
- case catch mnesia:dirty_index_read(session, USR, #session.usr) of
+ Res = case ejabberd_cluster:get_node({LUser, LServer}) of
+ Node when Node == node() ->
+ mnesia:dirty_index_read(session, USR, #session.usr);
+ Node ->
+ catch rpc:call(Node, mnesia, dirty_index_read,
+ [session, USR, #session.usr], 5000)
+ end,
+ case Res of
[#session{sid = {_, Pid}}] -> Pid;
_ -> none
end.
dirty_get_sessions_list() ->
- mnesia:dirty_select(
- session,
- [{#session{usr = '$1', _ = '_'},
- [],
- ['$1']}]).
+ Match = [{#session{usr = '$1', _ = '_'}, [], ['$1']}],
+ lists:flatmap(
+ fun(Node) when Node == node() ->
+ mnesia:dirty_select(session, Match);
+ (Node) ->
+ case catch rpc:call(Node, mnesia, dirty_select,
+ [session, Match], 5000) of
+ Ss when is_list(Ss) ->
+ Ss;
+ _ ->
+ []
+ end
+ end, ejabberd_cluster:get_nodes()).
dirty_get_my_sessions_list() ->
- mnesia:dirty_select(
- session,
- [{#session{sid = {'_', '$1'}, _ = '_'},
- [{'==', {node, '$1'}, node()}],
- ['$_']}]).
+ mnesia:dirty_match_object(#session{_ = '_'}).
-get_vh_session_list(Server) ->
+get_vh_my_session_list(Server) ->
LServer = jlib:nameprep(Server),
mnesia:dirty_select(
session,
@@ -222,19 +291,24 @@ get_vh_session_list(Server) ->
[{'==', {element, 2, '$1'}, LServer}],
['$1']}]).
+get_vh_session_list(Server) ->
+ lists:flatmap(
+ fun(Node) when Node == node() ->
+ get_vh_my_session_list(Server);
+ (Node) ->
+ case catch rpc:call(Node, ?MODULE, get_vh_my_session_list,
+ [Server], 5000) of
+ Ss when is_list(Ss) ->
+ Ss;
+ _ ->
+ []
+ end
+ end, ejabberd_cluster:get_nodes()).
+
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.
-
+ %% TODO
+ length(get_vh_session_list(Server)).
+
register_iq_handler(Host, XMLNS, Module, Fun) ->
ejabberd_sm ! {register_iq_handler, Host, XMLNS, Module, Fun}.
@@ -244,6 +318,51 @@ register_iq_handler(Host, XMLNS, Module, Fun, Opts) ->
unregister_iq_handler(Host, XMLNS) ->
ejabberd_sm ! {unregister_iq_handler, Host, XMLNS}.
+migrate(InitiatorNode, UpOrDown, After) ->
+ Ss = mnesia:dirty_select(
+ session,
+ [{#session{us = '$1', sid = {'_', '$2'}, _ = '_'},
+ [],
+ ['$$']}]),
+ lists:foreach(
+ fun([US, Pid]) ->
+ case ejabberd_cluster:get_node(US) of
+ Node when Node /= node() ->
+ if InitiatorNode == node() andalso UpOrDown == down ->
+ ejabberd_c2s:migrate_shutdown(
+ Pid, Node, random:uniform(After));
+ true ->
+ ejabberd_c2s:migrate(
+ Pid, Node, random:uniform(After))
+ end;
+ _ ->
+ ok
+ end
+ end, Ss).
+
+node_up(_Node) ->
+ copy_sessions(mnesia:dirty_first(session)).
+
+node_down(Node) when Node == node() ->
+ copy_sessions(mnesia:dirty_first(session));
+node_down(_) ->
+ ok.
+
+copy_sessions('$end_of_table') ->
+ ok;
+copy_sessions(Key) ->
+ case mnesia:dirty_read(session, Key) of
+ [#session{us = US} = Session] ->
+ case ejabberd_cluster:get_node_new(US) of
+ Node when node() /= Node ->
+ rpc:cast(Node, mnesia, dirty_write, [Session]);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end,
+ copy_sessions(mnesia:dirty_next(session, Key)).
%%====================================================================
%% gen_server callbacks
@@ -260,16 +379,15 @@ init([]) ->
update_tables(),
mnesia:create_table(session,
[{ram_copies, [node()]},
+ {local_content, true},
{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),
ets:new(sm_iqtable, [named_table]),
+ ejabberd_hooks:add(node_up, ?MODULE, node_up, 100),
+ ejabberd_hooks:add(node_down, ?MODULE, node_down, 100),
+ ejabberd_hooks:add(node_hash_update, ?MODULE, migrate, 100),
lists:foreach(
fun(Host) ->
ejabberd_hooks:add(roster_in_subscription, Host,
@@ -280,7 +398,7 @@ init([]) ->
ejabberd_sm, disconnect_removed_user, 100)
end, ?MYHOSTS),
ejabberd_commands:register_commands(commands()),
-
+ start_dispatchers(),
{ok, #state{}}.
%%--------------------------------------------------------------------
@@ -311,18 +429,21 @@ handle_cast(_Msg, State) ->
%% {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
-handle_info({route, From, To, Packet}, State) ->
- case catch do_route(From, To, Packet) of
- {'EXIT', Reason} ->
- ?ERROR_MSG("~p~nwhen processing: ~p",
- [Reason, {From, To, Packet}]);
- _ ->
- ok
+handle_info({route, From, To, Packet} = Msg, State) ->
+ case get_proc_num() of
+ N when N > 1 ->
+ #jid{luser = U, lserver = S} = To,
+ get_proc_by_hash({U, S}) ! Msg;
+ _ ->
+ case catch do_route(From, To, Packet) of
+ {'EXIT', Reason} ->
+ ?ERROR_MSG("~p~nwhen processing: ~p",
+ [Reason, {From, To, Packet}]);
+ _ ->
+ ok
+ end
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};
@@ -349,7 +470,11 @@ handle_info(_Info, State) ->
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
+ ejabberd_hooks:delete(node_up, ?MODULE, node_up, 100),
+ ejabberd_hooks:delete(node_down, ?MODULE, node_down, 100),
+ ejabberd_hooks:delete(node_hash_update, ?MODULE, migrate, 100),
ejabberd_commands:unregister_commands(commands()),
+ stop_dispatchers(),
ok.
%%--------------------------------------------------------------------
@@ -363,7 +488,7 @@ code_change(_OldVsn, State, _Extra) ->
%%% Internal functions
%%--------------------------------------------------------------------
-set_session(SID, User, Server, Resource, Priority, Info) ->
+set_session({_, Pid} = SID, User, Server, Resource, Priority, Info) ->
LUser = jlib:nodeprep(User),
LServer = jlib:nameprep(Server),
LResource = jlib:resourceprep(Resource),
@@ -376,40 +501,46 @@ set_session(SID, User, Server, Resource, Priority, Info) ->
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).
+ mnesia:sync_dirty(F),
+ case ejabberd_cluster:get_node_new(US) of
+ Node when node() /= Node ->
+ %% New node has just been added. But we may miss session records
+ %% copy procedure, so we copy the session record manually just
+ %% to make sure
+ rpc:cast(Node, mnesia, dirty_write,
+ [#session{sid = SID,
+ usr = USR,
+ us = US,
+ priority = Priority,
+ info = Info}]),
+ case ejabberd_cluster:get_node(US) of
+ Node when node() /= Node ->
+ %% Migration to new node has completed, and seems like
+ %% we missed it, so we migrate the session pid manually.
+ %% It is not a problem if we have already got migration
+ %% notification: dups are just ignored by the c2s pid.
+ ejabberd_c2s:migrate(Pid, Node, 0);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
do_route(From, To, Packet) ->
?DEBUG("session manager~n\tfrom ~p~n\tto ~p~n\tpacket ~P~n",
[From, To, Packet, 8]),
+ {U, S, _} = jlib:jid_tolower(To),
+ case ejabberd_cluster:get_node({U, S}) of
+ Node when Node /= node() ->
+ {?MODULE, Node} ! {route, From, To, Packet};
+ _ ->
+ do_route1(From, To, Packet)
+ end.
+
+do_route1(From, To, Packet) ->
#jid{user = User, server = Server,
luser = LUser, lserver = LServer, lresource = LResource} = To,
{xmlelement, Name, Attrs, _Els} = Packet,
@@ -772,6 +903,55 @@ user_resources(User, Server) ->
Resources = get_user_resources(User, Server),
lists:sort(Resources).
+get_proc_num() ->
+ erlang:system_info(logical_processors).
+
+get_proc_by_hash(Term) ->
+ N = erlang:phash2(Term, get_proc_num()) + 1,
+ get_proc(N).
+
+get_proc(N) ->
+ list_to_atom(atom_to_list(?MODULE) ++ "_" ++ integer_to_list(N)).
+
+start_dispatchers() ->
+ case get_proc_num() of
+ N when N > 1 ->
+ lists:foreach(
+ fun(I) ->
+ Pid = spawn(fun dispatch/0),
+ erlang:register(get_proc(I), Pid)
+ end, lists:seq(1, N));
+ _ ->
+ ok
+ end.
+
+stop_dispatchers() ->
+ case get_proc_num() of
+ N when N > 1 ->
+ lists:foreach(
+ fun(I) ->
+ get_proc(I) ! stop
+ end, lists:seq(1, N));
+ _ ->
+ ok
+ end.
+
+dispatch() ->
+ receive
+ {route, From, To, Packet} ->
+ case catch do_route(From, To, Packet) of
+ {'EXIT', Reason} ->
+ ?ERROR_MSG("~p~nwhen processing: ~p",
+ [Reason, {From, To, Packet}]);
+ _ ->
+ ok
+ end,
+ dispatch();
+ stop ->
+ stopped;
+ _ ->
+ dispatch()
+ end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Update Mnesia tables
@@ -802,4 +982,11 @@ update_tables() ->
mnesia:delete_table(local_session);
false ->
ok
+ end,
+ mnesia:delete_table(session_counter),
+ case catch mnesia:table_info(session, local_content) of
+ false ->
+ mnesia:delete_table(session);
+ _ ->
+ ok
end.
diff --git a/src/ejabberd_socket.erl b/src/ejabberd_socket.erl
index 254751b3e..1384205a5 100644
--- a/src/ejabberd_socket.erl
+++ b/src/ejabberd_socket.erl
@@ -44,6 +44,8 @@
get_peer_certificate/1,
get_verify_result/1,
close/1,
+ change_controller/2,
+ change_socket/2,
sockname/1, peername/1]).
-include("ejabberd.hrl").
@@ -135,29 +137,19 @@ connect(Addr, Port, Opts, Timeout) ->
end.
starttls(SocketData, TLSOpts) ->
- {ok, TLSSocket} = tls:tcp_to_tls(SocketData#socket_state.socket, TLSOpts),
- ejabberd_receiver:starttls(SocketData#socket_state.receiver, TLSSocket),
- SocketData#socket_state{socket = TLSSocket, sockmod = tls}.
+ starttls(SocketData, TLSOpts, undefined).
starttls(SocketData, TLSOpts, Data) ->
- {ok, TLSSocket} = tls:tcp_to_tls(SocketData#socket_state.socket, TLSOpts),
- ejabberd_receiver:starttls(SocketData#socket_state.receiver, TLSSocket),
- send(SocketData, Data),
+ {ok, TLSSocket} = ejabberd_receiver:starttls(
+ SocketData#socket_state.receiver, TLSOpts, Data),
SocketData#socket_state{socket = TLSSocket, sockmod = tls}.
compress(SocketData) ->
- {ok, ZlibSocket} = ejabberd_zlib:enable_zlib(
- SocketData#socket_state.sockmod,
- SocketData#socket_state.socket),
- ejabberd_receiver:compress(SocketData#socket_state.receiver, ZlibSocket),
- SocketData#socket_state{socket = ZlibSocket, sockmod = ejabberd_zlib}.
+ compress(SocketData, undefined).
compress(SocketData, Data) ->
- {ok, ZlibSocket} = ejabberd_zlib:enable_zlib(
- SocketData#socket_state.sockmod,
- SocketData#socket_state.socket),
- ejabberd_receiver:compress(SocketData#socket_state.receiver, ZlibSocket),
- send(SocketData, Data),
+ {ok, ZlibSocket} = ejabberd_receiver:compress(
+ SocketData#socket_state.receiver, Data),
SocketData#socket_state{socket = ZlibSocket, sockmod = ejabberd_zlib}.
reset_stream(SocketData) when is_pid(SocketData#socket_state.receiver) ->
@@ -166,10 +158,28 @@ reset_stream(SocketData) when is_atom(SocketData#socket_state.receiver) ->
(SocketData#socket_state.receiver):reset_stream(
SocketData#socket_state.socket).
+change_controller(#socket_state{receiver = Recv}, Pid) when is_pid(Recv) ->
+ ejabberd_receiver:setopts(Recv, [{active, false}]),
+ sync_events(Pid),
+ ejabberd_receiver:change_controller(Recv, Pid);
+change_controller(#socket_state{socket = Socket, receiver = Mod}, Pid) ->
+ Mod:setopts(Socket, [{active, false}]),
+ sync_events(Pid),
+ Mod:change_controller(Socket, Pid).
+
+change_socket(SocketData, Socket) ->
+ SocketData#socket_state{socket = Socket}.
+
%% sockmod=gen_tcp|tls|ejabberd_zlib
send(SocketData, Data) ->
- case catch (SocketData#socket_state.sockmod):send(
- SocketData#socket_state.socket, Data) of
+ Res = if node(SocketData#socket_state.receiver) == node() ->
+ catch (SocketData#socket_state.sockmod):send(
+ SocketData#socket_state.socket, Data);
+ true ->
+ catch ejabberd_receiver:send(
+ SocketData#socket_state.receiver, Data)
+ end,
+ case Res of
ok -> ok;
{error, timeout} ->
?INFO_MSG("Timeout on ~p:send",[SocketData#socket_state.sockmod]),
@@ -231,3 +241,21 @@ peername(#socket_state{sockmod = SockMod, socket = Socket}) ->
%%====================================================================
%% Internal functions
%%====================================================================
+%% dirty hack to relay queued messages from
+%% old owner to new owner. The idea is based
+%% on code of gen_tcp:controlling_process/2.
+sync_events(C2SPid) ->
+ receive
+ {'$gen_event', El} = Event when element(1, El) == xmlelement;
+ element(1, El) == xmlstreamstart;
+ element(1, El) == xmlstreamelement;
+ element(1, El) == xmlstreamend;
+ element(1, El) == xmlstreamerror ->
+ C2SPid ! Event,
+ sync_events(C2SPid);
+ closed ->
+ C2SPid ! closed,
+ sync_events(C2SPid)
+ after 0 ->
+ ok
+ end.
diff --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl
index 3c1f4177e..407c03e1d 100644
--- a/src/ejabberd_sup.erl
+++ b/src/ejabberd_sup.erl
@@ -42,13 +42,6 @@ init([]) ->
brutal_kill,
worker,
[ejabberd_hooks]},
- NodeGroups =
- {ejabberd_node_groups,
- {ejabberd_node_groups, start_link, []},
- permanent,
- brutal_kill,
- worker,
- [ejabberd_node_groups]},
SystemMonitor =
{ejabberd_system_monitor,
{ejabberd_system_monitor, start_link, []},
@@ -153,6 +146,14 @@ init([]) ->
infinity,
supervisor,
[ejabberd_tmp_sup]},
+ WSLoopSupervisor =
+ {ejabberd_wsloop_sup,
+ {ejabberd_tmp_sup, start_link,
+ [ejabberd_wsloop_sup, ejabberd_wsloop]},
+ permanent,
+ infinity,
+ supervisor,
+ [ejabberd_tmp_sup]},
FrontendSocketSupervisor =
{ejabberd_frontend_socket_sup,
{ejabberd_tmp_sup, start_link,
@@ -186,7 +187,6 @@ init([]) ->
[cache_tab_sup]},
{ok, {{one_for_one, 10, 1},
[Hooks,
- NodeGroups,
SystemMonitor,
Router,
SM,
diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl
new file mode 100644
index 000000000..0e958a14f
--- /dev/null
+++ b/src/ejabberd_xmlrpc.erl
@@ -0,0 +1,467 @@
+%%%----------------------------------------------------------------------
+%%% File : ejabberd_xmlrpc.erl
+%%% Author : Badlop <badlop@process-one.net>
+%%% Purpose : XML-RPC server that frontends ejabberd commands
+%%% Created : 21 Aug 2007 by Badlop <badlop@ono.com>
+%%% Id : $Id: ejabberd_xmlrpc.erl 595 2008-05-20 11:39:31Z badlop $
+%%%----------------------------------------------------------------------
+
+%%% TODO: Implement a command in ejabberdctl 'help COMMAND LANGUAGE' that shows
+%%% a coding example to call that command in a specific language (python, php).
+
+%%% TODO: Remove support for plaintext password
+
+%%% TODO: commands strings should be strings without ~n
+
+-module(ejabberd_xmlrpc).
+-author('badlop@process-one.net').
+
+-export([
+ start_listener/2,
+ handler/2,
+ socket_type/0
+ ]).
+
+-include("ejabberd.hrl").
+-include("mod_roster.hrl").
+-include("jlib.hrl").
+
+-record(state, {access_commands, auth = noauth, get_auth}).
+
+
+%% Test:
+
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_integer, [{struct, [{thisinteger, 5}]}]}).
+%% {ok,{response,[{struct,[{zero,0}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_string, [{struct, [{thisstring, "abcd"}]}]}).
+%% {ok,{response,[{struct,[{thatstring,"abcd"}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, tell_tuple_3integer, [{struct, [{thisstring, "abcd"}]}]}).
+%% {ok,{response,
+%% [{struct,
+%% [{thattuple,
+%% {array,
+%% [{struct,[{first,123}]},
+%% {struct,[{second,456}]},
+%% {struct,[{third,789}]}]}}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, pow, [{struct, [{base, 5}, {exponent, 7}]}]}).
+%% {ok,{response,[{struct,[{pow,78125}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, seq, [{struct, [{from, 3}, {to, 7}]}]}).
+%% {ok,{response,[{array,[{struct,[{intermediate,3}]},
+%% {struct,[{intermediate,4}]},
+%% {struct,[{intermediate,5}]},
+%% {struct,[{intermediate,6}]},
+%% {struct,[{intermediate,7}]}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, substrs, [{struct, [{word, "abcd"}]}]}).
+%% NO:
+%% {ok,{response,[{array,[{struct,[{miniword,"a"}]},
+%% {struct,[{miniword,"ab"}]},
+%% {struct,[{miniword,"abc"}]},
+%% {struct,[{miniword,"abcd"}]}]}]}}
+%% {ok,{response,
+%% [{struct,
+%% [{substrings,
+%% {array,
+%% [{struct,[{miniword,"a"}]},
+%% {struct,[{miniword,"ab"}]},
+%% {struct,[{miniword,"abc"}]},
+%% {struct,[{miniword,"abcd"}]}]}}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, splitjid, [{struct, [{jid, "abcd@localhost/work"}]}]}).
+%% {ok,{response,
+%% [{struct,
+%% [{jidparts,
+%% {array,
+%% [{struct,[{user,"abcd"}]},
+%% {struct,[{server,"localhost"}]},
+%% {struct,[{resource,"work"}]}]}}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string, [{struct, [{thisstring, "abc"}, {thisinteger, 55}]}]}).
+%% {ok,{response,
+%% [{struct,
+%% [{thistuple,
+%% {array,
+%% [{struct,[{thisinteger,55}]},
+%% {struct,[{thisstring,"abc"}]}]}}]}]}}
+%%
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_list_integer, [{struct, [{thislist, {array, [{struct, [{thisinteger, 55}, {thisinteger, 4567}]}]}}]}]}).
+%% {ok,{response,
+%% [{struct,
+%% [{thatlist,
+%% {array,
+%% [{struct,[{thatinteger,55}]},
+%% {struct,[{thatinteger,4567}]}]}}]}]}}
+%%
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_list_string, [{struct, [{thisinteger, 123456}, {thislist, {array, [{struct, [{thisstring, "abc"}, {thisstring, "bobo baba"}]}]}}]}]}).
+%% {ok,
+%% {response,
+%% [{struct,
+%% [{thistuple,
+%% {array,
+%% [{struct,[{thatinteger,123456}]},
+%% {struct,
+%% [{thatlist,
+%% {array,
+%% [{struct,[{thatstring,"abc"}]},
+%% {struct,[{thatstring,"bobo baba"}]}]}}]}]}}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_tuple_2integer, [{struct, [{thistuple, {array, [{struct, [{thisinteger1, 55}, {thisinteger2, 4567}]}]}}]}]}).
+%% {ok,{response,[{struct,[{zero,0}]}]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_isatils, [{struct,
+%% [{thisinteger, 123456990},
+%% {thisstring, "This is ISATILS"},
+%% {thisatom, "test_isatils"},
+%% {thistuple, {array, [{struct, [
+%% {listlen, 2},
+%% {thislist, {array, [{struct, [
+%% {contentstring, "word1"},
+%% {contentstring, "word 2"}
+%% ]}]}}
+%% ]}]}}
+%% ]}]}).
+%% {ok,{response,
+%% [{struct,
+%% [{results,
+%% {array,
+%% [{struct,[{thatinteger,123456990}]},
+%% {struct,[{thatstring,"This is ISATILS"}]},
+%% {struct,[{thatatom,"test_isatils"}]},
+%% {struct,
+%% [{thattuple,
+%% {array,
+%% [{struct,[{listlen,123456990}]},
+%% {struct,[{thatlist,...}]}]}}]}]}}]}]}}
+
+%% ecommand doesn't exist:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string2, [{struct, [{thisstring, "abc"}]}]}).
+%% {ok,{response,{fault,-1, "Unknown call: {call,echo_integer_string2,[{struct,[{thisstring,\"abc\"}]}]}"}}}
+%%
+%% Duplicated argument:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string, [{struct, [{thisstring, "abc"}, {thisinteger, 44}, {thisinteger, 55}]}]}).
+%% {ok,{response,{fault,-104, "Error -104\nAttribute 'thisinteger' duplicated:\n[{thisstring,\"abc\"},{thisinteger,44},{thisinteger,55}]"}}}
+%%
+%% Missing argument:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string, [{struct, [{thisstring, "abc"}]}]}).
+%% {ok,{response,{fault,-106, "Error -106\nRequired attribute 'thisinteger' not found:\n[{thisstring,\"abc\"}]"}}}
+%%
+%% Duplicated tuple element:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_tuple_2integer, [{struct, [{thistuple, {array, [{struct, [{thisinteger1, 55}, {thisinteger1, 66}, {thisinteger2, 4567}]}]}}]}]}).
+%% {ok,{response,{fault,-104, "Error -104\nAttribute 'thisinteger1' defined multiple times:\n[{thisinteger1,55},{thisinteger1,66},{thisinteger2,4567}]"}}}
+%%
+%% Missing element in tuple:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_tuple_2integer, [{struct, [{thistuple, {array, [{struct, [{thisinteger1, 55}, {thisintegerc, 66}, {thisinteger, 4567}]}]}}]}]}).
+%% {ok,{response,{fault,-106, "Error -106\nRequired attribute 'thisinteger2' not found:\n[{thisintegerc,66},{thisinteger,4567}]"}}}
+%%
+%% The ecommand crashed:
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, this_crashes, [{struct, []}]}).
+%% {ok,{response,{fault,-100, "Error -100\nA problem 'error' occurred executing the command this_crashes with arguments []: badarith"}}}
+
+
+%% -----------------------------
+%% Listener interface
+%% -----------------------------
+
+start_listener({Port, Ip, tcp = _TranportProtocol}, Opts) ->
+ %% get options
+ MaxSessions = gen_mod:get_opt(maxsessions, Opts, 10),
+ Timeout = gen_mod:get_opt(timeout, Opts, 5000),
+ AccessCommands = gen_mod:get_opt(access_commands, Opts, []),
+ GetAuth = case [ACom || {Ac, _, _} = ACom <- AccessCommands, Ac /= all] of
+ [] -> false;
+ _ -> true
+ end,
+
+ %% start the XML-RPC server
+ Handler = {?MODULE, handler},
+ State = #state{access_commands = AccessCommands, get_auth = GetAuth},
+ xmlrpc:start_link(Ip, Port, MaxSessions, Timeout, Handler, State).
+
+socket_type() ->
+ independent.
+
+
+%% -----------------------------
+%% Access verification
+%% -----------------------------
+
+%% @spec (AuthList) -> {User, Server, Password}
+%% where
+%% AuthList = [{user, string()}, {server, string()}, {password, string()}]
+%% It may throw: {error, missing_auth_arguments, Attr}
+get_auth(AuthList) ->
+ %% Check AuthList contains all the required data
+ [User, Server, Password] =
+ try get_attrs([user, server, password], AuthList) of
+ [U, S, P] -> [U, S, P]
+ catch
+ exit:{attribute_not_found, Attr, _} ->
+ throw({error, missing_auth_arguments, Attr})
+ end,
+ {User, Server, Password}.
+
+
+%% -----------------------------
+%% Handlers
+%% -----------------------------
+
+%% Call: Arguments: Returns:
+
+
+%% .............................
+%% Access verification
+
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [152]}).
+%% {ok,{response,{fault,-103, "Error -103\nRequired authentication: {call,echothis,[152]}"}}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "ada"}]}, 152]}).
+%% {ok,{response,{fault,-103,
+%% "Error -103\nAuthentication non valid: [{user,\"badlop\"},\n
+%% {server,\"localhost\"},\n
+%% {password,\"ada\"}]"}}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "ada90ada"}]}, 152]}).
+%% {ok,{response,[152]}}
+%%
+%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "79C1574A43BC995F2B145A299EF97277"}]}, 152]}).
+%% {ok,{response,[152]}}
+
+handler(#state{get_auth = true, auth = noauth} = State, {call, Method, [{struct, AuthList} | Arguments] = AllArgs}) ->
+ try get_auth(AuthList) of
+ Auth ->
+ handler(State#state{get_auth = false, auth = Auth}, {call, Method, Arguments})
+ catch
+ {error, missing_auth_arguments, _Attr} ->
+ handler(State#state{get_auth = false, auth = noauth}, {call, Method, AllArgs})
+ end;
+
+
+%% .............................
+%% Debug
+
+%% echothis String String
+handler(_State, {call, echothis, [A]}) ->
+ {false, {response, [A]}};
+
+%% echothisnew struct[{sentence, String}] struct[{repeated, String}]
+handler(_State, {call, echothisnew, [{struct, [{sentence, A}]}]}) ->
+ {false, {response, [{struct, [{repeated, A}]}]}};
+
+%% multhis struct[{a, Integer}, {b, Integer}] Integer
+handler(_State, {call, multhis, [{struct, [{a, A}, {b, B}]}]}) ->
+ {false, {response, [A*B]}};
+
+%% multhisnew struct[{a, Integer}, {b, Integer}] struct[{mu, Integer}]
+handler(_State, {call, multhisnew, [{struct, [{a, A}, {b, B}]}]}) ->
+ {false, {response, [{struct, [{mu, A*B}]}]}};
+
+%% .............................
+%% Statistics
+
+%% tellme_title String String
+handler(_State, {call, tellme_title, [A]}) ->
+ {false, {response, [get_title(A)]}};
+
+%% tellme_value String String
+handler(_State, {call, tellme_value, [A]}) ->
+ N = node(),
+ {false, {response, [get_value(N, A)]}};
+
+%% tellme String struct[{title, String}, {value. String}]
+handler(_State, {call, tellme, [A]}) ->
+ N = node(),
+ T = {title, get_title(A)},
+ V = {value, get_value(N, A)},
+ R = {struct, [T, V]},
+ {false, {response, [R]}};
+
+%% .............................
+%% ejabberd commands
+
+handler(State, {call, Command, []}) ->
+ %% The XMLRPC request may not contain a struct parameter,
+ %% but our internal functions need such struct, even if it's empty
+ %% So let's add it and do a recursive call:
+ handler(State, {call, Command, [{struct, []}]});
+
+handler(State, {call, Command, [{struct, AttrL}]} = Payload) ->
+ case ejabberd_commands:get_command_format(Command) of
+ {error, command_unknown} ->
+ build_fault_response(-112, "Unknown call: ~p", [Payload]);
+ {ArgsF, ResultF} ->
+ try_do_command(State#state.access_commands, State#state.auth, Command, AttrL, ArgsF, ResultF)
+ end;
+
+%% If no other guard matches
+handler(_State, Payload) ->
+ build_fault_response(-112, "Unknown call: ~p", [Payload]).
+
+
+%% -----------------------------
+%% Command
+%% -----------------------------
+
+try_do_command(AccessCommands, Auth, Command, AttrL, ArgsF, ResultF) ->
+ try do_command(AccessCommands, Auth, Command, AttrL, ArgsF, ResultF) of
+ {command_result, ResultFormatted} ->
+ {false, {response, [ResultFormatted]}}
+ catch
+ exit:{duplicated_attribute, ExitAt, ExitAtL} ->
+ build_fault_response(-114, "Attribute '~p' duplicated:~n~p", [ExitAt, ExitAtL]);
+ exit:{attribute_not_found, ExitAt, ExitAtL} ->
+ build_fault_response(-116, "Required attribute '~p' not found:~n~p", [ExitAt, ExitAtL]);
+ exit:{additional_unused_args, ExitAtL} ->
+ build_fault_response(-120, "The call provided additional unused arguments:~n~p", [ExitAtL]);
+ throw:Why ->
+ build_fault_response(-118, "A problem '~p' occurred executing the command ~p with arguments~n~p", [Why, Command, AttrL])
+ end.
+
+build_fault_response(Code, ParseString, ParseArgs) ->
+ FaultString = "Error " ++ integer_to_list(Code) ++ "\n" ++
+ lists:flatten(io_lib:format(ParseString, ParseArgs)),
+ ?WARNING_MSG(FaultString, []), %% Show Warning message in ejabberd log file
+ {false, {response, {fault, Code, FaultString}}}.
+
+do_command(AccessCommands, Auth, Command, AttrL, ArgsF, ResultF) ->
+ ArgsFormatted = format_args(AttrL, ArgsF),
+ Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command, ArgsFormatted),
+ ResultFormatted = format_result(Result, ResultF),
+ {command_result, ResultFormatted}.
+
+
+%%-----------------------------
+%% Format arguments
+%%-----------------------------
+
+get_attrs(Attribute_names, L) ->
+ [get_attr(A, L) || A <- Attribute_names].
+
+get_attr(A, L) ->
+ case lists:keysearch(A, 1, L) of
+ {value, {A, Value}} -> Value;
+ false ->
+ %% Report the error and then force a crash
+ exit({attribute_not_found, A, L})
+ end.
+
+%% Get an element from a list and delete it.
+%% The element must be defined once and only once,
+%% otherwise the function crashes on purpose.
+get_elem_delete(A, L) ->
+ case proplists:get_all_values(A, L) of
+ [Value] ->
+ {Value, proplists:delete(A, L)};
+ [_, _ | _] ->
+ %% Crash reporting the error
+ exit({duplicated_attribute, A, L});
+ [] ->
+ %% Report the error and then force a crash
+ exit({attribute_not_found, A, L})
+ end.
+
+
+format_args(Args, ArgsFormat) ->
+ {ArgsRemaining, R} =
+ lists:foldl(
+ fun({ArgName, ArgFormat}, {Args1, Res}) ->
+ {ArgValue, Args2} = get_elem_delete(ArgName, Args1),
+ Formatted = format_arg(ArgValue, ArgFormat),
+ {Args2, Res ++ [Formatted]}
+ end,
+ {Args, []},
+ ArgsFormat),
+ case ArgsRemaining of
+ [] -> R;
+ L when is_list(L) ->
+ exit({additional_unused_args, L})
+ end.
+format_arg({array, [{struct, Elements}]}, {list, {ElementDefName, ElementDefFormat}})
+ when is_list(Elements) ->
+ lists:map(
+ fun({ElementName, ElementValue}) ->
+ true = (ElementDefName == ElementName),
+ format_arg(ElementValue, ElementDefFormat)
+ end,
+ Elements);
+format_arg({array, [{struct, Elements}]}, {tuple, ElementsDef})
+ when is_list(Elements) ->
+ FormattedList = format_args(Elements, ElementsDef),
+ list_to_tuple(FormattedList);
+format_arg({array, Elements}, {list, ElementsDef})
+ when is_list(Elements) and is_atom(ElementsDef) ->
+ [format_arg(Element, ElementsDef) || Element <- Elements];
+format_arg(Arg, integer)
+ when is_integer(Arg) ->
+ Arg;
+format_arg(Arg, string)
+ when is_list(Arg) ->
+ Arg.
+
+
+%% -----------------------------
+%% Result
+%% -----------------------------
+
+format_result({error, Error}, _) ->
+ throw({error, Error});
+
+format_result(String, string) ->
+ lists:flatten(String);
+
+format_result(Atom, {Name, atom}) ->
+ {struct, [{Name, atom_to_list(Atom)}]};
+
+format_result(Int, {Name, integer}) ->
+ {struct, [{Name, Int}]};
+
+format_result(String, {Name, string}) ->
+ {struct, [{Name, lists:flatten(String)}]};
+
+format_result(Code, {Name, rescode}) ->
+ {struct, [{Name, make_status(Code)}]};
+
+format_result({Code, Text}, {Name, restuple}) ->
+ {struct, [{Name, make_status(Code)},
+ {text, lists:flatten(Text)}]};
+
+%% Result is a list of something: [something()]
+format_result(Elements, {Name, {list, ElementsDef}}) ->
+ FormattedList = lists:map(
+ fun(Element) ->
+ format_result(Element, ElementsDef)
+ end,
+ Elements),
+ {struct, [{Name, {array, FormattedList}}]};
+
+%% Result is a tuple with several elements: {something1(), something2(), ...}
+format_result(ElementsTuple, {Name, {tuple, ElementsDef}}) ->
+ ElementsList = tuple_to_list(ElementsTuple),
+ ElementsAndDef = lists:zip(ElementsList, ElementsDef),
+ FormattedList = lists:map(
+ fun({Element, ElementDef}) ->
+ format_result(Element, ElementDef)
+ end,
+ ElementsAndDef),
+ {struct, [{Name, {array, FormattedList}}]}.
+%% TODO: should be struct instead of array?
+
+
+make_status(ok) -> 0;
+make_status(true) -> 0;
+make_status(false) -> 1;
+make_status(error) -> 1;
+make_status(_) -> 1.
+
+
+%% -----------------------------
+%% Internal
+%% -----------------------------
+
+get_title(A) -> mod_statsdx:get_title(A).
+get_value(N, A) -> mod_statsdx:get(N, [A]).
diff --git a/src/ejabberdctl.template b/src/ejabberdctl.template
index 0960f9aff..4286c7c2b 100644
--- a/src/ejabberdctl.template
+++ b/src/ejabberdctl.template
@@ -143,6 +143,7 @@ export EXEC_CMD
# start server
start ()
{
+ check_start
$EXEC_CMD "$ERL \
$NAME $ERLANG_NODE \
-noinput -detached \
@@ -189,6 +190,7 @@ debug ()
# start interactive server
live ()
{
+ check_start
echo "--------------------------------------------------------------------"
echo ""
echo "IMPORTANT: ejabberd is going to start in LIVE (interactive) mode."
@@ -217,6 +219,13 @@ live ()
$ERLANG_OPTS $ARGS \"$@\""
}
+etop()
+{
+ $EXEC_CMD "$ERL \
+ $NAME debug-${TTY}-${ERLANG_NODE} \
+ -hidden -s etop -s erlang halt -output text -node $ERLANG_NODE"
+}
+
help ()
{
echo ""
@@ -337,6 +346,26 @@ stop_epmd()
epmd -names | grep -q name || epmd -kill
}
+# make sure node not already running and node name unregistered
+check_start()
+{
+ epmd -names | grep -q $NODE && {
+ ps ux | grep -v grep | grep -q $ERLANG_NODE && {
+ echo "ejabberd is already running."
+ exit 4
+ } || {
+ ps ux | grep beam | grep -v "grep beam" && {
+ echo "ejabberd node is registered, but no ejabberd process has been found."
+ echo "can not kill epmd as other erlang nodes are running."
+ echo "please stop all erlang nodes, and call 'epmd -kill'."
+ exit 5
+ } || {
+ epmd -kill
+ }
+ }
+ }
+}
+
# allow sync calls
wait_for_status()
{
@@ -366,6 +395,7 @@ case $ARGS in
' start') start;;
' debug') debug;;
' live') live;;
+ ' etop') etop;;
' started') wait_for_status 0 30 2;; # wait 30x2s before timeout
' stopped') wait_for_status 3 15 2; stop_epmd;; # wait 15x2s before timeout
*) ctl $ARGS;;
diff --git a/src/etop_defs.hrl b/src/etop_defs.hrl
new file mode 100644
index 000000000..664de6197
--- /dev/null
+++ b/src/etop_defs.hrl
@@ -0,0 +1,29 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2002-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+-define(SYSFORM,
+ " ~-72w~10s~n"
+ " Load: cpu ~8w Memory: total ~8w binary ~8w~n"
+ " procs~8w processes~8w code ~8w~n"
+ " runq ~8w atom ~8w ets ~8w~n").
+
+-record(opts, {node=node(), port = 8415, accum = false, intv = 5000, lines = 10,
+ width = 700, height = 340, sort = runtime, tracing = on,
+ %% Other state information
+ out_mod=etop_gui, out_proc, server, host, tracer, store,
+ accum_tab, remote}).
diff --git a/src/etop_tr.erl b/src/etop_tr.erl
new file mode 100644
index 000000000..c5ae79cc0
--- /dev/null
+++ b/src/etop_tr.erl
@@ -0,0 +1,130 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2002-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+-module(etop_tr).
+-author('siri@erix.ericsson.se').
+
+%%-compile(export_all).
+-export([setup_tracer/1,stop_tracer/1,reader/1]).
+-import(etop,[getopt/2]).
+
+-include("etop_defs.hrl").
+
+setup_tracer(Config) ->
+ TraceNode = getopt(node,Config),
+ RHost = rpc:call(TraceNode, net_adm, localhost, []),
+ Store = ets:new(?MODULE, [set, public]),
+
+ %% We can only trace one process anyway kill the old one.
+ case erlang:whereis(dbg) of
+ undefined ->
+ case rpc:call(TraceNode, erlang, whereis, [dbg]) of
+ undefined -> fine;
+ Pid ->
+ exit(Pid, kill)
+ end;
+ Pid ->
+ exit(Pid,kill)
+ end,
+
+ dbg:tracer(TraceNode,port,dbg:trace_port(ip,{getopt(port,Config),5000})),
+ dbg:p(all,[running,timestamp]),
+ T = dbg:get_tracer(TraceNode),
+ Config#opts{tracer=T,host=RHost,store=Store}.
+
+stop_tracer(_Config) ->
+ dbg:p(all,clear),
+ dbg:stop(),
+ ok.
+
+
+
+reader(Config) ->
+ Host = getopt(host, Config),
+ Port = getopt(port, Config),
+
+ {ok, Sock} = gen_tcp:connect(Host, Port, [{active, false}]),
+ spawn_link(fun() -> reader_init(Sock,getopt(store,Config),nopid) end).
+
+
+%%%%%%%%%%%%%% Socket reader %%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+reader_init(Sock, Store, Last) ->
+ process_flag(priority, high),
+ reader(Sock, Store, Last).
+
+reader(Sock, Store, Last) ->
+ Data = get_data(Sock),
+ New = handle_data(Last, Data, Store),
+ reader(Sock, Store, New).
+
+handle_data(_, {_, Pid, in, _, Time}, _) ->
+ {Pid,Time};
+handle_data({Pid,Time1}, {_, Pid, out, _, Time2}, Store) ->
+ Elapsed = elapsed(Time1, Time2),
+ case ets:member(Store,Pid) of
+ true -> ets:update_counter(Store, Pid, Elapsed);
+ false -> ets:insert(Store,{Pid,Elapsed})
+ end,
+ nopid;
+handle_data(_W, {drop, D}, _) -> %% Error case we are missing data here!
+ io:format("Erlang top dropped data ~p~n", [D]),
+ nopid;
+handle_data(nopid, {_, _, out, _, _}, _Store) ->
+ %% ignore - there was probably just a 'drop'
+ nopid;
+handle_data(_, G, _) ->
+ %% io:format("Erlang top got garbage ~p~n", [G]),
+ nopid.
+
+elapsed({Me1, S1, Mi1}, {Me2, S2, Mi2}) ->
+ Me = (Me2 - Me1) * 1000000,
+ S = (S2 - S1 + Me) * 1000000,
+ Mi2 - Mi1 + S.
+
+
+%%%%%% Socket helpers %%%%
+get_data(Sock) ->
+ [Op | BESiz] = my_ip_read(Sock, 5),
+ Siz = get_be(BESiz),
+ case Op of
+ 0 ->
+ B = list_to_binary(my_ip_read(Sock, Siz)),
+ binary_to_term(B);
+ 1 ->
+ {drop, Siz};
+ Else ->
+ exit({'bad trace tag', Else})
+ end.
+
+get_be([A,B,C,D]) ->
+ A * 16777216 + B * 65536 + C * 256 + D.
+
+my_ip_read(Sock,N) ->
+ case gen_tcp:recv(Sock, N) of
+ {ok, Data} ->
+ case length(Data) of
+ N ->
+ Data;
+ X ->
+ Data ++ my_ip_read(Sock, N - X)
+ end;
+ _Else ->
+ exit(eof)
+ end.
+
diff --git a/src/expat_erl.c b/src/expat_erl.c
index 2b7db3f6d..1e45d1448 100644
--- a/src/expat_erl.c
+++ b/src/expat_erl.c
@@ -217,6 +217,35 @@ static ErlDrvSSizeT expat_erl_control(ErlDrvData drv_data,
case PARSE_FINAL_COMMAND:
ei_x_new_with_version(&event_buf);
ei_x_new(&xmlns_buf);
+#ifdef ENABLE_FLASH_HACK
+ /* Flash hack - Flash clients send a null byte after the stanza. Remove that... */
+ {
+ int i;
+ int found_null = 0;
+
+ /* Maybe the Flash client sent many stanzas in one packet.
+ If so, there is a null byte between every stanza. */
+ for (i = 0; i < len; i++) {
+ if (buf[i] == '\0') {
+ buf[i] = ' ';
+ found_null = 1;
+ }
+ }
+
+ /* And also remove the closing slash if this is a
+ flash:stream element. Assume that flash:stream is the
+ last element in the packet, and entirely contained in
+ it. This requires that a null byte has been found. */
+ if (found_null && strstr(buf, "<flash:stream"))
+ /* buf[len - 1] is an erased null byte.
+ buf[len - 2] is >
+ buf[len - 3] is / (maybe)
+ */
+ if (buf[len - 3] == '/')
+ buf[len - 3] = ' ';
+ }
+#endif /* ENABLE_FLASH_HACK */
+
res = XML_Parse(d->parser, buf, len, command == PARSE_FINAL_COMMAND);
if(!res)
diff --git a/src/floodcheck.erl b/src/floodcheck.erl
new file mode 100644
index 000000000..f2a20e46a
--- /dev/null
+++ b/src/floodcheck.erl
@@ -0,0 +1,205 @@
+%%%-------------------------------------------------------------------
+%%% File : floodcheck.erl
+%%% Author : Christophe Romain <christophe.romain@process-one.net>
+%%% Description :
+%%%
+%%% Created : 11 Sep 2008 by Christophe Romain <christophe.romain@process-one.net>
+%%%-------------------------------------------------------------------
+-module(floodcheck).
+
+-behaviour(gen_server).
+
+%% API
+-export([start_link/0, stop/0]).
+-export([monitor/5, demonitor/1, interval/1, check/0]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+
+-define(DEFAULT_INTERVAL, 300). %% check every 5mn
+-define(SERVER, ?MODULE).
+
+-record(state, {timer, interval, monitors}).
+-record(monitor, {id, pid, ref, info, rule, value, handler}).
+
+%%====================================================================
+%% API
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
+%% Description: Starts the server
+%%--------------------------------------------------------------------
+start_link() ->
+ case whereis(?SERVER) of
+ undefined -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []);
+ Pid -> {ok, Pid}
+ end.
+
+stop() ->
+ gen_server:call(?SERVER, stop).
+
+monitor(Id, Pid, Info, Spec, {Mod, Fun}) ->
+ gen_server:cast(?SERVER, {monitor, Id, Pid, Info, Spec, {Mod, Fun}}).
+
+demonitor(Id) ->
+ gen_server:cast(?SERVER, {demonitor, Id}).
+
+interval(Value) ->
+ gen_server:cast(?SERVER, {interval, Value}).
+
+check() ->
+ gen_server:call(?SERVER, check).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% Description: Initiates the server
+%%--------------------------------------------------------------------
+init([]) ->
+ Timer = erlang:send_after(?DEFAULT_INTERVAL*1000, ?SERVER, monitor),
+ {ok, #state{timer=Timer, interval=?DEFAULT_INTERVAL, monitors=[]}}.
+
+%%--------------------------------------------------------------------
+%% 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(check, _From, State) ->
+ Reply = lists:map(fun(#monitor{id=Id}=M) ->
+ {Id, check(M)}
+ end, State#state.monitors),
+ {reply, Reply, State};
+handle_call(stop, _From, State) ->
+ erlang:cancel_timer(State#state.timer),
+ {stop, normal, ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: handle_cast(Msg, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% Description: Handling cast messages
+%%--------------------------------------------------------------------
+handle_cast({monitor, Id, Pid, Info, Spec, Handler}, State) ->
+ Monitors = State#state.monitors,
+ Ref = erlang:monitor(process, Pid),
+ {Rule, Value} = case Spec of
+ {Op, V} -> {Op, V};
+ V -> {'>', V}
+ end,
+ Monitor = #monitor{id=Id, pid=Pid, ref=Ref, info=Info, rule=Rule, value=Value, handler=Handler},
+ New = case lists:keysearch(Id, #monitor.id, Monitors) of
+ {value, #monitor{ref=OldRef}} ->
+ erlang:demonitor(OldRef),
+ lists:keyreplace(Id, #monitor.id, Monitors, Monitor);
+ _ ->
+ [Monitor|Monitors]
+ end,
+ {noreply, State#state{monitors=New}};
+handle_cast({demonitor, Id}, State) ->
+ Monitors = State#state.monitors,
+ New = case lists:keysearch(Id, #monitor.id, Monitors) of
+ {value, #monitor{ref=Ref}} ->
+ erlang:demonitor(Ref),
+ lists:keydelete(Id, #monitor.id, Monitors);
+ _ ->
+ Monitors
+ end,
+ {noreply, State#state{monitors=New}};
+handle_cast({interval, Value}, State) ->
+ erlang:cancel_timer(State#state.timer),
+ Timer = erlang:send_after(Value*1000, ?SERVER, monitor),
+ {noreply, State#state{timer=Timer, interval=Value}};
+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({'DOWN', Ref, _Type, _Pid, _Info}, State) ->
+ Monitors = State#state.monitors,
+ New = lists:keydelete(Ref, #monitor.ref, Monitors),
+ {noreply, State#state{monitors=New}};
+handle_info(monitor, State) ->
+ lists:foreach(fun(#monitor{id=Id, pid=Pid, info=Info, handler={Mod, Fun}}=M) ->
+ case check(M) of
+ ok -> ok;
+ Value -> spawn(Mod, Fun, [Id, Pid, Info, Value])
+ end
+ end, State#state.monitors),
+ Timer = erlang:send_after(State#state.interval*1000, ?SERVER, monitor),
+ {noreply, State#state{timer=Timer}};
+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
+%%--------------------------------------------------------------------
+
+check(#monitor{pid=Pid, info=Info, rule=Rule, value=Value}) ->
+ case catch process_info(Pid, Info) of
+ {Info, Actual} ->
+ Check = case Info of
+ messages -> byte_size(term_to_binary(Actual));
+ dictionary -> byte_size(term_to_binary(Actual));
+ _ -> Actual
+ end,
+ case Rule of
+ '>' ->
+ if Check > Value -> Value;
+ true -> ok
+ end;
+ '<' ->
+ if Check < Value -> Value;
+ true -> ok
+ end;
+ '=' ->
+ if Check == Value -> Value;
+ true -> ok
+ end;
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end.
+
+%%% Documentation
+%%% authorized Info
+%%% message_queue_len: number of messages
+%%% messages: messages queue size in bytes
+%%% dictionary: dictionary size in bytes
+%%% total_heap_size: total size in words of all heap fragments
+%%% heap_size: size in words of youngest heap generation
+%%% stack_size: stack size in words
+%%% reductions: number of reductions executed by the process
+%%% memory: process size in bytes
diff --git a/src/http_p1.erl b/src/http_p1.erl
new file mode 100644
index 000000000..1a8a1e630
--- /dev/null
+++ b/src/http_p1.erl
@@ -0,0 +1,337 @@
+%%%----------------------------------------------------------------------
+%%% File : http_p1.erl
+%%% Author : Emilio Bustos <ebustos@process-one.net>
+%%% Purpose : Provide a common API for inets / lhttpc / ibrowse
+%%% Created : 29 Jul 2010 by Emilio Bustos <ebustos@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2010 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+-module(http_p1).
+-author('ebustos@process-one.net').
+
+-export([
+ start/0,
+ stop/0,
+ get/1,
+ get/2,
+ post/2,
+ post/3,
+ request/3,
+ request/4,
+ request/5
+]).
+
+% -define(USE_INETS, 1).
+% -define(USE_LHTTPC, 1).
+% -define(USE_IBROWSE, 1).
+% inets used as default if none specified
+
+-ifdef(USE_IBROWSE).
+ -define(start(), start_ibrowse()).
+ -define(request(M, U, H, B, O), request_ibrowse(M, U, H, B, O)).
+ -define(stop(), stop_ibrowse()).
+-else.
+ -ifdef(USE_LHTTPC).
+ -define(start(), start_lhttpc()).
+ -define(request(M, U, H, B, O), request_lhttpc(M, U, H, B, O)).
+ -define(stop(), stop_lhttpc()).
+ -else.
+ -define(start(), start_inets()).
+ -define(request(M, U, H, B, O), request_inets(M, U, H, B, O)).
+ -define(stop(), stop_inets()).
+ -endif.
+-endif.
+
+-type header() :: {string() | atom(), string()}.
+-type headers() :: [header()].
+
+-type option() ::
+ {connect_timeout, timeout()} |
+ {timeout, timeout()} |
+
+ {send_retry, non_neg_integer()} |
+ {partial_upload, non_neg_integer() | infinity} |
+ {partial_download, pid(), non_neg_integer() | infinity}.
+
+-type options() :: [option()].
+
+-type result() :: {ok, {{pos_integer(), string()}, headers(), string()}} |
+ {error, atom()}.
+
+%% @spec () -> ok | {error, Reason}
+%% Reason = term()
+%% @doc
+%% Start the application.
+%% This is a helper function that will start the corresponding backend.
+%% It allows the library to be started using the `-s' flag.
+%% For instance:
+%% `$ erl -s http_p1'
+%%
+%% @end
+-spec start() -> ok | {error, any()}.
+start() ->
+ ?start().
+
+start_inets()->
+ inets:start(),
+ ssl:start().
+
+start_lhttpc()->
+ application:start(crypto),
+ application:start(ssl),
+ lhttpc:start().
+
+start_ibrowse()->
+ ibrowse:start(),
+ ssl:start().
+
+%% @spec () -> ok | {error, Reason}
+%% Reason = term()
+%% @doc
+%% Stops the application.
+%% This is a helper function that will stop the corresponding backend.
+%%
+%% @end
+-spec stop() -> ok | {error, any()}.
+stop() ->
+ ?stop().
+
+stop_inets()->
+ inets:stop(),
+ ssl:stop().
+
+stop_lhttpc()->
+ lhttpc:stop(),
+ application:stop(ssl).
+
+stop_ibrowse()->
+ ibrowse:stop().
+
+%% @spec (URL) -> Result
+%% URL = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a GET request.
+%% Would be the same as calling `request(get, URL, [])',
+%% that is {@link request/3} with an empty header list.
+%% @end
+%% @see request/3
+-spec get(string()) -> result().
+get(URL) ->
+ request(get, URL, []).
+
+%% @spec (URL, Hdrs) -> Result
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a GET request.
+%% Would be the same as calling `request(get, URL, Hdrs)'.
+%% @end
+%% @see request/3
+-spec get(string(), headers()) -> result().
+get(URL, Hdrs) ->
+ request(get, URL, Hdrs).
+
+%% @spec (URL, RequestBody) -> Result
+%% URL = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a POST request with form data.
+%% Would be the same as calling
+%% `request(post, URL, [{"content-type", "x-www-form-urlencoded"}], Body)'.
+%% @end
+%% @see request/4
+-spec post(string(), string()) -> result().
+post(URL, Body) ->
+ request(post, URL, [{"content-type", "x-www-form-urlencoded"}], Body).
+
+%% @spec (URL, Hdrs, RequestBody) -> Result
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a POST request.
+%% Would be the same as calling
+%% `request(post, URL, Hdrs, Body)'.
+%% @end
+%% @see request/4
+-spec post(string(), headers(), string()) -> result().
+post(URL, Hdrs, Body) ->
+ NewHdrs = case [X || {X,_}<-Hdrs, string:to_lower(X) == "content-type"] of
+ [] ->
+ [{"content-type", "x-www-form-urlencoded"} | Hdrs];
+ _ ->
+ Hdrs
+ end,
+ request(post, URL, NewHdrs, Body).
+
+%% @spec (Method, URL, Hdrs) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request without a body.
+%% Would be the same as calling `request(Method, URL, Hdrs, [], [])',
+%% that is {@link request/5} with an empty body.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers()) -> result().
+request(Method, URL, Hdrs) ->
+ request(Method, URL, Hdrs, [], []).
+
+%% @spec (Method, URL, Hdrs, RequestBody) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request with a body.
+%% Would be the same as calling
+%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5}
+%% with no options.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers(), string()) -> result().
+request(Method, URL, Hdrs, Body) ->
+ request(Method, URL, Hdrs, Body, []).
+
+%% @spec (Method, URL, Hdrs, RequestBody, Options) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Options = [Option]
+%% Option = {timeout, Milliseconds | infinity} |
+%% {connect_timeout, Milliseconds | infinity} |
+%% {socket_options, [term()]} |
+
+%% Milliseconds = integer()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request with a body.
+%% Would be the same as calling
+%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5}
+%% with no options.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers(), string(), options()) -> result().
+request(Method, URL, Hdrs, Body, Opts) ->
+% ?DEBUG("Making request with headers: ~p~n~n", [Hdrs]),
+% Headers = lists:map(fun({H, V}) ->
+% H2 = if
+% is_atom(H) ->
+% string:to_lower(atom_to_list(H));
+% is_list(H) ->
+% string:to_lower(H);
+% true ->
+% H
+% end,
+% {H2, V}
+% end, Hdrs),
+ ?request(Method, URL, Hdrs, Body, Opts).
+
+request_inets(Method, URL, Hdrs, Body, Opts) ->
+ Request = case Method of
+ get ->
+ {URL, Hdrs};
+ head ->
+ {URL, Hdrs};
+ _ -> % post, etc.
+ {URL, Hdrs, proplists:get_value("content-type", Hdrs, []), Body}
+ end,
+ Options = case proplists:get_value(timeout, Opts, infinity) of
+ infinity ->
+ proplists:delete(timeout, Opts);
+ _ ->
+ Opts
+ end,
+ case http:request(Method, Request, Options, []) of
+ {ok, {{_, Status, _}, Headers, Response}} ->
+ {ok, Status, Headers, Response};
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+request_lhttpc(Method, URL, Hdrs, Body, Opts) ->
+ TimeOut = proplists:get_value(timeout, Opts, infinity),
+ SockOpt = proplists:get_value(socket_options, Opts, []),
+ Options = [{connect_options, SockOpt} | proplists:delete(timeout, Opts)],
+ case lhttpc:request(URL, Method, Hdrs, Body, TimeOut, Options) of
+ {ok, {{Status, _Reason}, Headers, Response}} ->
+ {ok, Status, Headers, binary_to_list(Response)};
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+request_ibrowse(Method, URL, Hdrs, Body, Opts) ->
+ TimeOut = proplists:get_value(timeout, Opts, infinity),
+ Options = [{inactivity_timeout, TimeOut} | proplists:delete(timeout, Opts)],
+ case ibrowse:send_req(URL, Hdrs, Method, Body, Options) of
+ {ok, Status, Headers, Response} ->
+ {ok, list_to_integer(Status), Headers, Response};
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+% ibrowse {response_format, response_format()} |
+% Options - [option()]
+% Option - {sync, boolean()} | {stream, StreamTo} | {body_format, body_format()} | {full_result,
+% boolean()} | {headers_as_is, boolean()}
+%body_format() = string() | binary()
+% The body_format option is only valid for the synchronous request and the default is string.
+% When making an asynchronous request the body will always be received as a binary.
+% lhttpc: always binary
diff --git a/src/jlib.hrl b/src/jlib.hrl
index ae5862d86..c86120528 100644
--- a/src/jlib.hrl
+++ b/src/jlib.hrl
@@ -68,6 +68,7 @@
-define(NS_EJABBERD_CONFIG, "ejabberd:config").
-define(NS_STREAM, "http://etherx.jabber.org/streams").
+-define(NS_FLASH_STREAM, "http://www.jabber.com/streams/flash").
-define(NS_STANZAS, "urn:ietf:params:xml:ns:xmpp-stanzas").
-define(NS_STREAMS, "urn:ietf:params:xml:ns:xmpp-streams").
@@ -126,6 +127,8 @@
?STANZA_ERROR("401", "auth", "not-authorized")).
-define(ERR_PAYMENT_REQUIRED,
?STANZA_ERROR("402", "auth", "payment-required")).
+-define(ERR_POLICY_VIOLATION,
+ ?STANZA_ERROR("405", "cancel", "policy-violation")).
-define(ERR_RECIPIENT_UNAVAILABLE,
?STANZA_ERROR("404", "wait", "recipient-unavailable")).
-define(ERR_REDIRECT,
@@ -210,117 +213,117 @@
?ERRT_CONFLICT(Lang, "Resource conflict")).
--define(STREAM_ERROR(Condition),
+-define(STREAM_ERROR(Condition, Cdata),
{xmlelement, "stream:error",
[],
- [{xmlelement, Condition, [{"xmlns", ?NS_STREAMS}], []}]}).
+ [{xmlelement, Condition, [{"xmlns", ?NS_STREAMS}],
+ [{xmlcdata, Cdata}]}]}).
-define(SERR_BAD_FORMAT,
- ?STREAM_ERROR("bad-format")).
+ ?STREAM_ERROR("bad-format", "")).
-define(SERR_BAD_NAMESPACE_PREFIX,
- ?STREAM_ERROR("bad-namespace-prefix")).
+ ?STREAM_ERROR("bad-namespace-prefix", "")).
-define(SERR_CONFLICT,
- ?STREAM_ERROR("conflict")).
+ ?STREAM_ERROR("conflict", "")).
-define(SERR_CONNECTION_TIMEOUT,
- ?STREAM_ERROR("connection-timeout")).
+ ?STREAM_ERROR("connection-timeout", "")).
-define(SERR_HOST_GONE,
- ?STREAM_ERROR("host-gone")).
+ ?STREAM_ERROR("host-gone", "")).
-define(SERR_HOST_UNKNOWN,
- ?STREAM_ERROR("host-unknown")).
+ ?STREAM_ERROR("host-unknown", "")).
-define(SERR_IMPROPER_ADDRESSING,
- ?STREAM_ERROR("improper-addressing")).
+ ?STREAM_ERROR("improper-addressing", "")).
-define(SERR_INTERNAL_SERVER_ERROR,
- ?STREAM_ERROR("internal-server-error")).
+ ?STREAM_ERROR("internal-server-error", "")).
-define(SERR_INVALID_FROM,
- ?STREAM_ERROR("invalid-from")).
+ ?STREAM_ERROR("invalid-from", "")).
-define(SERR_INVALID_ID,
- ?STREAM_ERROR("invalid-id")).
+ ?STREAM_ERROR("invalid-id", "")).
-define(SERR_INVALID_NAMESPACE,
- ?STREAM_ERROR("invalid-namespace")).
+ ?STREAM_ERROR("invalid-namespace", "")).
-define(SERR_INVALID_XML,
- ?STREAM_ERROR("invalid-xml")).
+ ?STREAM_ERROR("invalid-xml", "")).
-define(SERR_NOT_AUTHORIZED,
- ?STREAM_ERROR("not-authorized")).
+ ?STREAM_ERROR("not-authorized", "")).
-define(SERR_POLICY_VIOLATION,
- ?STREAM_ERROR("policy-violation")).
+ ?STREAM_ERROR("policy-violation", "")).
-define(SERR_REMOTE_CONNECTION_FAILED,
- ?STREAM_ERROR("remote-connection-failed")).
+ ?STREAM_ERROR("remote-connection-failed", "")).
-define(SERR_RESOURSE_CONSTRAINT,
- ?STREAM_ERROR("resource-constraint")).
+ ?STREAM_ERROR("resource-constraint", "")).
-define(SERR_RESTRICTED_XML,
- ?STREAM_ERROR("restricted-xml")).
-% TODO: include hostname or IP
--define(SERR_SEE_OTHER_HOST,
- ?STREAM_ERROR("see-other-host")).
+ ?STREAM_ERROR("restricted-xml", "")).
+-define(SERR_SEE_OTHER_HOST(Host),
+ ?STREAM_ERROR("see-other-host", Host)).
-define(SERR_SYSTEM_SHUTDOWN,
- ?STREAM_ERROR("system-shutdown")).
+ ?STREAM_ERROR("system-shutdown", "")).
-define(SERR_UNSUPPORTED_ENCODING,
- ?STREAM_ERROR("unsupported-encoding")).
+ ?STREAM_ERROR("unsupported-encoding", "")).
-define(SERR_UNSUPPORTED_STANZA_TYPE,
- ?STREAM_ERROR("unsupported-stanza-type")).
+ ?STREAM_ERROR("unsupported-stanza-type", "")).
-define(SERR_UNSUPPORTED_VERSION,
- ?STREAM_ERROR("unsupported-version")).
+ ?STREAM_ERROR("unsupported-version", "")).
-define(SERR_XML_NOT_WELL_FORMED,
- ?STREAM_ERROR("xml-not-well-formed")).
+ ?STREAM_ERROR("xml-not-well-formed", "")).
%-define(SERR_,
-% ?STREAM_ERROR("")).
+% ?STREAM_ERROR("", "")).
--define(STREAM_ERRORT(Condition, Lang, Text),
+-define(STREAM_ERRORT(Condition, Cdata, Lang, Text),
{xmlelement, "stream:error",
[],
- [{xmlelement, Condition, [{"xmlns", ?NS_STREAMS}], []},
+ [{xmlelement, Condition, [{"xmlns", ?NS_STREAMS}],
+ [{xmlcdata, Cdata}]},
{xmlelement, "text", [{"xml:lang", Lang}, {"xmlns", ?NS_STREAMS}],
[{xmlcdata, translate:translate(Lang, Text)}]}]}).
-define(SERRT_BAD_FORMAT(Lang, Text),
- ?STREAM_ERRORT("bad-format", Lang, Text)).
+ ?STREAM_ERRORT("bad-format", "", Lang, Text)).
-define(SERRT_BAD_NAMESPACE_PREFIX(Lang, Text),
- ?STREAM_ERRORT("bad-namespace-prefix", Lang, Text)).
+ ?STREAM_ERRORT("bad-namespace-prefix", "", Lang, Text)).
-define(SERRT_CONFLICT(Lang, Text),
- ?STREAM_ERRORT("conflict", Lang, Text)).
+ ?STREAM_ERRORT("conflict", "", Lang, Text)).
-define(SERRT_CONNECTION_TIMEOUT(Lang, Text),
- ?STREAM_ERRORT("connection-timeout", Lang, Text)).
+ ?STREAM_ERRORT("connection-timeout", "", Lang, Text)).
-define(SERRT_HOST_GONE(Lang, Text),
- ?STREAM_ERRORT("host-gone", Lang, Text)).
+ ?STREAM_ERRORT("host-gone", "", Lang, Text)).
-define(SERRT_HOST_UNKNOWN(Lang, Text),
- ?STREAM_ERRORT("host-unknown", Lang, Text)).
+ ?STREAM_ERRORT("host-unknown", "", Lang, Text)).
-define(SERRT_IMPROPER_ADDRESSING(Lang, Text),
- ?STREAM_ERRORT("improper-addressing", Lang, Text)).
+ ?STREAM_ERRORT("improper-addressing", "", Lang, Text)).
-define(SERRT_INTERNAL_SERVER_ERROR(Lang, Text),
- ?STREAM_ERRORT("internal-server-error", Lang, Text)).
+ ?STREAM_ERRORT("internal-server-error", "", Lang, Text)).
-define(SERRT_INVALID_FROM(Lang, Text),
- ?STREAM_ERRORT("invalid-from", Lang, Text)).
+ ?STREAM_ERRORT("invalid-from", "", Lang, Text)).
-define(SERRT_INVALID_ID(Lang, Text),
- ?STREAM_ERRORT("invalid-id", Lang, Text)).
+ ?STREAM_ERRORT("invalid-id", "", Lang, Text)).
-define(SERRT_INVALID_NAMESPACE(Lang, Text),
- ?STREAM_ERRORT("invalid-namespace", Lang, Text)).
+ ?STREAM_ERRORT("invalid-namespace", "", Lang, Text)).
-define(SERRT_INVALID_XML(Lang, Text),
- ?STREAM_ERRORT("invalid-xml", Lang, Text)).
+ ?STREAM_ERRORT("invalid-xml", "", Lang, Text)).
-define(SERRT_NOT_AUTHORIZED(Lang, Text),
- ?STREAM_ERRORT("not-authorized", Lang, Text)).
+ ?STREAM_ERRORT("not-authorized", "", Lang, Text)).
-define(SERRT_POLICY_VIOLATION(Lang, Text),
- ?STREAM_ERRORT("policy-violation", Lang, Text)).
+ ?STREAM_ERRORT("policy-violation", "", Lang, Text)).
-define(SERRT_REMOTE_CONNECTION_FAILED(Lang, Text),
- ?STREAM_ERRORT("remote-connection-failed", Lang, Text)).
+ ?STREAM_ERRORT("remote-connection-failed", "", Lang, Text)).
-define(SERRT_RESOURSE_CONSTRAINT(Lang, Text),
- ?STREAM_ERRORT("resource-constraint", Lang, Text)).
+ ?STREAM_ERRORT("resource-constraint", "", Lang, Text)).
-define(SERRT_RESTRICTED_XML(Lang, Text),
- ?STREAM_ERRORT("restricted-xml", Lang, Text)).
-% TODO: include hostname or IP
--define(SERRT_SEE_OTHER_HOST(Lang, Text),
- ?STREAM_ERRORT("see-other-host", Lang, Text)).
+ ?STREAM_ERRORT("restricted-xml", "", Lang, Text)).
+-define(SERRT_SEE_OTHER_HOST(Host, Lang, Text),
+ ?STREAM_ERRORT("see-other-host", Host, Lang, Text)).
-define(SERRT_SYSTEM_SHUTDOWN(Lang, Text),
- ?STREAM_ERRORT("system-shutdown", Lang, Text)).
+ ?STREAM_ERRORT("system-shutdown", "", Lang, Text)).
-define(SERRT_UNSUPPORTED_ENCODING(Lang, Text),
- ?STREAM_ERRORT("unsupported-encoding", Lang, Text)).
+ ?STREAM_ERRORT("unsupported-encoding", "", Lang, Text)).
-define(SERRT_UNSUPPORTED_STANZA_TYPE(Lang, Text),
- ?STREAM_ERRORT("unsupported-stanza-type", Lang, Text)).
+ ?STREAM_ERRORT("unsupported-stanza-type", "", Lang, Text)).
-define(SERRT_UNSUPPORTED_VERSION(Lang, Text),
- ?STREAM_ERRORT("unsupported-version", Lang, Text)).
+ ?STREAM_ERRORT("unsupported-version", "", Lang, Text)).
-define(SERRT_XML_NOT_WELL_FORMED(Lang, Text),
- ?STREAM_ERRORT("xml-not-well-formed", Lang, Text)).
+ ?STREAM_ERRORT("xml-not-well-formed", "", Lang, Text)).
%-define(SERRT_(Lang, Text),
-% ?STREAM_ERRORT("", Lang, Text)).
+% ?STREAM_ERRORT("", "", Lang, Text)).
-record(jid, {user, server, resource,
diff --git a/src/licence.hrl b/src/licence.hrl
new file mode 100644
index 000000000..a82dab15e
--- /dev/null
+++ b/src/licence.hrl
@@ -0,0 +1 @@
+-define(IS_VALID, true).
diff --git a/src/log.hrl b/src/log.hrl
new file mode 100644
index 000000000..0f025e570
--- /dev/null
+++ b/src/log.hrl
@@ -0,0 +1,38 @@
+%% Copyright (C) 2003 Joakim Grebenö <jocke@gleipnir.com>.
+%% All rights reserved.
+%%
+%% Redistribution and use in source and binary forms, with or without
+%% modification, are permitted provided that the following conditions
+%% are met:
+%%
+%% 1. Redistributions of source code must retain the above copyright
+%% notice, this list of conditions and the following disclaimer.
+%% 2. 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.
+%%
+%% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
+
+-define(INFO_LOG(Reason),
+ error_logger:info_report({?MODULE, ?LINE, Reason})).
+
+-define(ERROR_LOG(Reason),
+ error_logger:error_report({?MODULE, ?LINE, Reason})).
+
+-ifdef(DEBUG).
+-define(DEBUG_LOG(Reason),
+ error_logger:info_report({debug, ?MODULE, ?LINE, Reason})).
+-else.
+-define(DEBUG_LOG(Reason), ok).
+-endif.
diff --git a/src/mochiglobal.erl b/src/mochiglobal.erl
new file mode 100644
index 000000000..c740b8781
--- /dev/null
+++ b/src/mochiglobal.erl
@@ -0,0 +1,107 @@
+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2010 Mochi Media, Inc.
+%% @doc Abuse module constant pools as a "read-only shared heap" (since erts 5.6)
+%% <a href="http://www.erlang.org/pipermail/erlang-questions/2009-March/042503.html">[1]</a>.
+-module(mochiglobal).
+-author("Bob Ippolito <bob@mochimedia.com>").
+-export([get/1, get/2, put/2, delete/1]).
+
+-spec get(atom()) -> any() | undefined.
+%% @equiv get(K, undefined)
+get(K) ->
+ get(K, undefined).
+
+-spec get(atom(), T) -> any() | T.
+%% @doc Get the term for K or return Default.
+get(K, Default) ->
+ get(K, Default, key_to_module(K)).
+
+get(_K, Default, Mod) ->
+ try Mod:term()
+ catch error:undef ->
+ Default
+ end.
+
+-spec put(atom(), any()) -> ok.
+%% @doc Store term V at K, replaces an existing term if present.
+put(K, V) ->
+ put(K, V, key_to_module(K)).
+
+put(_K, V, Mod) ->
+ Bin = compile(Mod, V),
+ code:purge(Mod),
+ code:load_binary(Mod, atom_to_list(Mod) ++ ".erl", Bin),
+ ok.
+
+-spec delete(atom()) -> boolean().
+%% @doc Delete term stored at K, no-op if non-existent.
+delete(K) ->
+ delete(K, key_to_module(K)).
+
+delete(_K, Mod) ->
+ code:purge(Mod),
+ code:delete(Mod).
+
+-spec key_to_module(atom()) -> atom().
+key_to_module(K) ->
+ list_to_atom("mochiglobal:" ++ atom_to_list(K)).
+
+-spec compile(atom(), any()) -> binary().
+compile(Module, T) ->
+ {ok, Module, Bin} = compile:forms(forms(Module, T),
+ [verbose, report_errors]),
+ Bin.
+
+-spec forms(atom(), any()) -> [erl_syntax:syntaxTree()].
+forms(Module, T) ->
+ [erl_syntax:revert(X) || X <- term_to_abstract(Module, term, T)].
+
+-spec term_to_abstract(atom(), atom(), any()) -> [erl_syntax:syntaxTree()].
+term_to_abstract(Module, Getter, T) ->
+ [%% -module(Module).
+ erl_syntax:attribute(
+ erl_syntax:atom(module),
+ [erl_syntax:atom(Module)]),
+ %% -export([Getter/0]).
+ erl_syntax:attribute(
+ erl_syntax:atom(export),
+ [erl_syntax:list(
+ [erl_syntax:arity_qualifier(
+ erl_syntax:atom(Getter),
+ erl_syntax:integer(0))])]),
+ %% Getter() -> T.
+ erl_syntax:function(
+ erl_syntax:atom(Getter),
+ [erl_syntax:clause([], none, [erl_syntax:abstract(T)])])].
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+get_put_delete_test() ->
+ K = '$$test$$mochiglobal',
+ delete(K),
+ ?assertEqual(
+ bar,
+ get(K, bar)),
+ try
+ ?MODULE:put(K, baz),
+ ?assertEqual(
+ baz,
+ get(K, bar)),
+ ?MODULE:put(K, wibble),
+ ?assertEqual(
+ wibble,
+ ?MODULE:get(K))
+ after
+ delete(K)
+ end,
+ ?assertEqual(
+ bar,
+ get(K, bar)),
+ ?assertEqual(
+ undefined,
+ ?MODULE:get(K)),
+ ok.
+-endif.
diff --git a/src/mod_ack.erl b/src/mod_ack.erl
new file mode 100644
index 000000000..7268b549e
--- /dev/null
+++ b/src/mod_ack.erl
@@ -0,0 +1,420 @@
+%%%-------------------------------------------------------------------
+%%% File : mod_ack.erl
+%%% Author : Mickael Remond <mremond@process-one.net>
+%%% Description : Implements reliable message delivery
+%%% Note: this module depends on mod_caps
+%%% Created : 12 Mar 2010 by Mickael Remond <mremond@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2010 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+-module(mod_ack).
+
+-behaviour(gen_server).
+-behaviour(gen_mod).
+
+%% API
+-export([start/2, stop/1, start_link/2]).
+
+-export([user_send_packet/3,
+ offline_message/3,
+ delayed_message/3,
+ remove_connection/3,
+ feature_inspect_packet/4]).
+
+%% gen_server callbacks
+-export([init/1,
+ handle_info/2,
+ handle_call/3,
+ handle_cast/2,
+ terminate/2,
+ code_change/3]).
+
+-include("jlib.hrl").
+-include("ejabberd.hrl").
+
+-define(PROCNAME, ejabberd_mod_ack).
+-define(ACK_TIMEOUT, 60). %% seconds
+-define(DICT, dict).
+
+-ifndef(NS_RECEIPTS).
+-define(NS_RECEIPTS, "urn:xmpp:receipts").
+-endif.
+-ifndef(NS_PING).
+-define(NS_PING, "urn:xmpp:ping").
+-endif.
+-ifndef(NS_P1_PUSHED).
+-define(NS_P1_PUSHED, "p1:pushed").
+-endif.
+
+-record(state, {host, timers = ?DICT:new(), timeout}).
+
+%%====================================================================
+%% API
+%%====================================================================
+start_link(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+start(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ ChildSpec =
+ {Proc,
+ {?MODULE, start_link, [Host, Opts]},
+ 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),
+ supervisor:terminate_child(ejabberd_sup, Proc),
+ supervisor:delete_child(ejabberd_sup, Proc).
+
+%% TODO: Make ack on server receive optional ?
+user_send_packet(From, To, {xmlelement, "message", _Attrs, _Els} = Packet) ->
+ case has_receipt_request(Packet) of
+ {true, _} ->
+ process_ack_request("on-sender-server", From, To, Packet);
+ false ->
+ case has_receipt_response(Packet) of
+ {true, ID} ->
+ Server = From#jid.lserver,
+ del_timer(Server, {message, ID}, From);
+ false ->
+ do_nothing
+ end
+ end;
+user_send_packet(From, _To, {xmlelement, "iq", Attrs, _Els}) ->
+ case xml:get_attr_s("id", Attrs) of
+ "" ->
+ ok;
+ ID ->
+ Server = From#jid.lserver,
+ del_timer(Server, {iq, ID}, From)
+ end;
+user_send_packet(_From, _To, _Packet) ->
+ do_nothing.
+
+offline_message(From, To, Packet) ->
+ process_ack_request("offline", From, To, Packet),
+ ok.
+
+delayed_message(From, To, Packet) ->
+ process_ack_request("delayed", From, To, Packet),
+ ok.
+
+feature_inspect_packet(JID, Server,
+ {xmlelement, "presence", _, _} = Pres,
+ {xmlelement, "message", Attrs, _} = El) ->
+ HasReceipts = has_receipt_request(El),
+ ReceiptsSupported = are_receipts_supported(Pres),
+ ?DEBUG("feature_inspect_packet:~n"
+ "** JID: ~p~n"
+ "** Has receipts: ~p~n"
+ "** Receipts supported: ~p~n"
+ "** Pres: ~p~n"
+ "** El: ~p",
+ [JID, HasReceipts, ReceiptsSupported, Pres, El]),
+ Type = xml:get_attr_s("type", Attrs),
+ case HasReceipts of
+ _ when Type == "error" ->
+ ok;
+ {true, ID} ->
+ case {jlib:string_to_jid(xml:get_attr_s("from", Attrs)),
+ jlib:string_to_jid(xml:get_attr_s("to", Attrs))} of
+ {#jid{} = From, #jid{} = To} ->
+ Pkt = {From, To, El},
+ case ReceiptsSupported of
+ true ->
+ add_timer(Server, {message, ID}, JID, Pkt);
+ false ->
+ ping(From, To, Server, JID, El);
+ unknown ->
+ process_ack_request("unreliable", From, To, El)
+ end;
+ _ ->
+ ?WARNING_MSG("message doesn't have 'from' or 'to'"
+ " attribute:~n** El: ~p", [El])
+ end;
+ _ ->
+ ok
+ end;
+feature_inspect_packet(_User, _Server, _Pres, _El) ->
+ ok.
+
+remove_connection({_, C2SPid}, #jid{lserver = Host}, _Info) ->
+ gen_server:cast(gen_mod:get_module_proc(Host, ?PROCNAME), {del, C2SPid}).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+init([Host, Opts]) ->
+ Timeout = timer:seconds(gen_mod:get_opt(timeout, Opts, ?ACK_TIMEOUT)),
+ ejabberd_hooks:add(user_send_packet, Host,
+ ?MODULE, user_send_packet, 20),
+ ejabberd_hooks:add(offline_message_hook, Host,
+ ?MODULE, offline_message, 20),
+ ejabberd_hooks:add(delayed_message_hook, Host,
+ ?MODULE, delayed_message, 20),
+ ejabberd_hooks:add(feature_inspect_packet, Host,
+ ?MODULE, feature_inspect_packet, 150),
+ ejabberd_hooks:add(sm_remove_connection_hook, Host,
+ ?MODULE, remove_connection, 20),
+ ejabberd_hooks:add(sm_remove_migrated_connection_hook, Host,
+ ?MODULE, remove_connection, 20),
+ {ok, #state{host = Host, timeout = Timeout}}.
+
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Req, _From, State) ->
+ {reply, {error, badarg}, State}.
+
+handle_cast({add, ID, Pid, Packet}, State) ->
+ TRef = erlang:start_timer(State#state.timeout, self(), {ID, Pid}),
+ Timers = insert(Pid, ID, {TRef, Packet}, State#state.timers),
+ {noreply, State#state{timers = Timers}};
+handle_cast({del, ID, Pid}, State) ->
+ case lookup(Pid, ID, State#state.timers) of
+ {ok, {TRef, {From, To, {xmlelement, _, Attrs, _}}}} ->
+ cancel_timer(TRef),
+ Timers = delete(Pid, ID, State#state.timers),
+ case ID of
+ {iq, _} ->
+ MsgID = xml:get_attr_s("id", Attrs),
+ Message = {xmlelement, "message", [{"id", MsgID}],
+ [{xmlelement, "received",
+ [{"xmlns", ?NS_RECEIPTS}, {"id", MsgID}], []}]},
+ ejabberd_router:route(To, From, Message);
+ _ ->
+ ok
+ end,
+ {noreply, State#state{timers = Timers}};
+ error ->
+ {noreply, State}
+ end;
+handle_cast({del, Pid}, State) ->
+ lists:foreach(
+ fun({_, _, {TRef, {From, To, El}}}) ->
+ cancel_timer(TRef),
+ El1 = xml:remove_subtags(El, "x", {"xmlns", ?NS_P1_PUSHED}),
+ El2 = xml:append_subtags(
+ El1, [{xmlelement, "x", [{"xmlns", ?NS_P1_PUSHED}], []}]),
+ ?DEBUG("Resending message:~n"
+ "** From: ~p~n"
+ "** To: ~p~n"
+ "** El: ~p",
+ [From, To, El2]),
+ ejabberd_router:route(From, To, El2)
+ end, to_list(Pid, State#state.timers)),
+ Timers = delete(Pid, State#state.timers),
+ {noreply, State#state{timers = Timers}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info({timeout, _TRef, {ID, Pid}}, State) ->
+ case lookup(Pid, ID, State#state.timers) of
+ {ok, _} ->
+ MRef = erlang:monitor(process, Pid),
+ catch ejabberd_c2s:stop(Pid),
+ receive
+ {'DOWN', MRef, process, Pid, _Reason}->
+ ok
+ after 5 ->
+ catch exit(Pid, kill)
+ end,
+ erlang:demonitor(MRef, [flush]),
+ handle_cast({del, Pid}, State);
+ error ->
+ {noreply, State}
+ end;
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ Host = State#state.host,
+ ejabberd_hooks:delete(user_send_packet, Host,
+ ?MODULE, user_send_packet, 20),
+ ejabberd_hooks:delete(offline_message_hook, Host,
+ ?MODULE, offline_message, 20),
+ ejabberd_hooks:delete(delayed_message_hook, Host,
+ ?MODULE, delayed_message, 20),
+ ejabberd_hooks:delete(feature_inspect_packet, Host,
+ ?MODULE, feature_inspect_packet, 150),
+ ejabberd_hooks:delete(sm_remove_connection_hook, Host,
+ ?MODULE, remove_connection, 20),
+ ejabberd_hooks:delete(sm_remove_migrated_connection_hook, Host,
+ ?MODULE, remove_connection, 20),
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
+process_ack_request(AckTagName,
+ #jid{lserver=LServer} = From, To,
+ {xmlelement, "message", _Attrs, _Els} = Packet) ->
+ case has_receipt_request(Packet) of
+ {true, ID} ->
+ BareTo = jlib:jid_remove_resource(To),
+ Message = {xmlelement, "message", [{"id", ID}],
+ [{xmlelement, AckTagName,
+ [{"xmlns", ?NS_RECEIPTS},
+ {"server", LServer}, {"id", ID}], []}]},
+ ejabberd_router:route(BareTo, From, Message);
+ false ->
+ do_nothing
+ end.
+
+has_receipt_request(Packet) ->
+ has_receipt(Packet, "request").
+
+has_receipt_response(Packet) ->
+ has_receipt(Packet, "received").
+
+has_receipt({xmlelement, "message", MsgAttrs, _} = Packet, Type) ->
+ case xml:get_attr_s("id", MsgAttrs) of
+ "" ->
+ case Type of
+ "request" -> false; %% Message must have an ID to ask a request for ack.
+ "received" ->
+ case xml:get_subtag(Packet, "received") of
+ false ->
+ false;
+ {xmlelement, _Name, Attrs, _Els} ->
+ case xml:get_attr_s("xmlns", Attrs) of
+ ?NS_RECEIPTS ->
+ case xml:get_attr_s("id", Attrs) of
+ "" -> false;
+ SubTagID -> {true, SubTagID}
+ end;
+ _ -> false
+ end
+ end
+ end;
+ ID ->
+ case xml:get_subtag(Packet, Type) of
+ false ->
+ false;
+ {xmlelement, _Name, Attrs, _Els} ->
+ case xml:get_attr_s("xmlns", Attrs) of
+ ?NS_RECEIPTS ->
+ case xml:get_attr_s("id", Attrs) of
+ "" -> {true, ID};
+ SubTagID -> {true, SubTagID}
+ end;
+ _ ->
+ false
+ end
+ end
+ end.
+
+are_receipts_supported(undefined) ->
+ unknown;
+are_receipts_supported({xmlelement, "presence", _, Els}) ->
+ case mod_caps:read_caps(Els) of
+ nothing ->
+ unknown;
+ Caps ->
+ lists:member(?NS_RECEIPTS, mod_caps:get_features(Caps))
+ end.
+
+ping(From, To, Server, JID, El) ->
+ ID = randoms:get_string(),
+ add_timer(Server, {iq, ID}, JID, {From, To, El}),
+ ejabberd_router:route(jlib:make_jid("", Server, ""), JID,
+ {xmlelement, "iq",
+ [{"type", "get"}, {"id", ID}],
+ [{xmlelement, "query", [{"xmlns", ?NS_PING}], []}]}).
+
+add_timer(Host, ID, JID, Packet) ->
+ {U, S, R} = jlib:jid_tolower(JID),
+ C2SPid = ejabberd_sm:get_session_pid(U, S, R),
+ gen_server:cast(gen_mod:get_module_proc(Host, ?PROCNAME),
+ {add, ID, C2SPid, Packet}).
+
+del_timer(Host, ID, JID) ->
+ {U, S, R} = jlib:jid_tolower(JID),
+ C2SPid = ejabberd_sm:get_session_pid(U, S, R),
+ gen_server:cast(gen_mod:get_module_proc(Host, ?PROCNAME),
+ {del, ID, C2SPid}).
+
+cancel_timer(TRef) ->
+ case erlang:cancel_timer(TRef) of
+ false ->
+ receive
+ {timeout, TRef, _} ->
+ ok
+ after 0 ->
+ ok
+ end;
+ _ ->
+ ok
+ end.
+
+lookup(Pid, Key, Queue) ->
+ case ?DICT:find(Pid, Queue) of
+ {ok, Treap} ->
+ case treap:lookup(Key, Treap) of
+ {ok, _, Val} ->
+ {ok, Val};
+ error ->
+ error
+ end;
+ error ->
+ error
+ end.
+
+insert(Pid, Key, Val, Queue) ->
+ Treap = case ?DICT:find(Pid, Queue) of
+ {ok, Treap1} ->
+ Treap1;
+ error ->
+ nil
+ end,
+ ?DICT:store(Pid, treap:insert(Key, now(), Val, Treap), Queue).
+
+delete(Pid, Key, Queue) ->
+ case ?DICT:find(Pid, Queue) of
+ {ok, Treap} ->
+ NewTreap = treap:delete(Key, Treap),
+ case treap:is_empty(NewTreap) of
+ true ->
+ ?DICT:erase(Pid, Queue);
+ false ->
+ ?DICT:store(Pid, NewTreap, Queue)
+ end;
+ error ->
+ Queue
+ end.
+
+delete(Pid, Queue) ->
+ ?DICT:erase(Pid, Queue).
+
+to_list(Pid, Queue) ->
+ case ?DICT:find(Pid, Queue) of
+ {ok, Treap} ->
+ treap:to_list(Treap);
+ error ->
+ []
+ end.
diff --git a/src/mod_admin_p1.erl b/src/mod_admin_p1.erl
new file mode 100644
index 000000000..0077e5fd1
--- /dev/null
+++ b/src/mod_admin_p1.erl
@@ -0,0 +1,1347 @@
+%%%-------------------------------------------------------------------
+%%% File : mod_admin_p1.erl
+%%% Author : Badlop / Mickael Remond / Christophe Romain
+%%% Purpose : Administrative functions and commands for ProcessOne customers
+%%% Created : 21 May 2008 by Badlop <badlop@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2010 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+
+%%% @doc Administrative functions and commands for ProcessOne customers
+%%%
+%%% This ejabberd module defines and registers many ejabberd commands
+%%% that can be used for performing administrative tasks in ejabberd.
+%%%
+%%% The documentation of all those commands can be read using ejabberdctl
+%%% in the shell.
+%%%
+%%% The commands can be executed using any frontend to ejabberd commands.
+%%% Currently ejabberd_xmlrpc and ejabberdctl. Using ejabberd_xmlrpc it is possible
+%%% to call any ejabberd command. However using ejabberdctl not all commands
+%%% can be called.
+
+%%% Changelog:
+%%%
+%%% 0.8 - 26 September 2008 - badlop
+%%% - added patch for parameter 'Push'
+%%%
+%%% 0.7 - 20 August 2008 - badlop
+%%% - module converted to ejabberd commands
+%%%
+%%% 0.6 - 02 June 2008 - cromain
+%%% - add user existance checking
+%%% - improve parameter checking
+%%% - allow orderless parameter
+%%%
+%%% 0.5 - 17 March 2008 - cromain
+%%% - add user changing and higher level methods
+%%%
+%%% 0.4 - 18 February 2008 - cromain
+%%% - add roster handling
+%%% - add message sending
+%%% - code and api clean-up
+%%%
+%%% 0.3 - 18 October 2007 - cromain
+%%% - presence improvement
+%%% - add new functionality
+%%%
+%%% 0.2 - 4 March 2006 - mremond
+%%% - Code clean-up
+%%% - Made it compatible with current ejabberd SVN version
+%%%
+%%% 0.1.2 - 28 December 2005
+%%% - Now compatible with ejabberd 1.0.0
+%%% - The XMLRPC server is started only once, not once for every virtual host
+%%% - Added comments for handlers. Every available handler must be explained
+%%%
+
+-module(mod_admin_p1).
+-author('ProcessOne').
+
+-export([start/2, stop/1,
+ %% Erlang
+ restart_module/2,
+ %% Accounts
+ create_account/3,
+ delete_account/2,
+ change_password/3,
+ rename_account/4,
+ check_users_registration/1,
+ %% Sessions
+ get_presence/2,
+ get_resources/2,
+ %% Vcard
+ set_nickname/3,
+ %% Roster
+ add_rosteritem/6,
+ delete_rosteritem/3,
+ add_rosteritem_groups/5,
+ del_rosteritem_groups/5,
+ modify_rosteritem_groups/6,
+ link_contacts/6,
+ unlink_contacts/2,
+ link_contacts/7, unlink_contacts/3, % Versions with Push parameter
+ get_roster/2,
+ get_roster_with_presence/2,
+ add_contacts/3,
+ remove_contacts/3,
+ %% PubSub
+ update_status/4,
+ delete_status/3,
+ %% Transports
+ transport_register/5,
+ %% Stanza
+ send_chat/3,
+ send_message/4,
+ send_stanza/3
+ ]).
+
+-include("ejabberd.hrl").
+-include("ejabberd_commands.hrl").
+-include("mod_roster.hrl").
+-include("jlib.hrl").
+
+-ifdef(EJABBERD1).
+-record(session, {sid, usr, us, priority}). %% ejabberd 1.1.x
+-else.
+-record(session, {sid, usr, us, priority, info}). %% ejabberd 2.x.x
+-endif.
+
+start(_Host, _Opts) ->
+ ejabberd_commands:register_commands(commands()).
+
+stop(_Host) ->
+ ejabberd_commands:unregister_commands(commands()).
+
+%%%
+%%% Register commands
+%%%
+
+commands() ->
+ [
+ #ejabberd_commands{name = restart_module, tags = [erlang],
+ desc = "Stop an ejabberd module, reload code and start",
+ module = ?MODULE, function = restart_module,
+ args = [{module, string}, {host, string}],
+ result = {res, rescode}},
+
+ %% Similar to ejabberd_admin register
+ #ejabberd_commands{name = create_account, tags = [accounts],
+ desc = "Create an ejabberd user account",
+ longdesc = "This command is similar to 'register'.",
+ module = ?MODULE, function = create_account,
+ args = [{user, string}, {server, string},
+ {password, string}],
+ result = {res, integer}},
+
+ %% Similar to ejabberd_admin unregister
+ #ejabberd_commands{name = delete_account, tags = [accounts],
+ desc = "Remove an account from the server",
+ longdesc = "This command is similar to 'unregister'.",
+ module = ?MODULE, function = delete_account,
+ args = [{user, string}, {server, string}],
+ result = {res, integer}},
+
+ #ejabberd_commands{name = rename_account, tags = [accounts],
+ desc = "Change an acount name",
+ longdesc = "Creates a new account "
+ "and copies the roster from the old one, and updates the rosters of his contacts. "
+ "Offline messages and private storage are lost.",
+ module = ?MODULE, function = rename_account,
+ args = [{user, string}, {server, string},
+ {newuser, string}, {newserver, string}],
+ result = {res, integer}},
+
+ %% This command is also implemented in mod_admin_contrib
+ #ejabberd_commands{name = change_password, tags = [accounts],
+ desc = "Change the password on behalf of the given user",
+ module = ?MODULE, function = change_password,
+ args = [{user, string}, {server, string},
+ {newpass, string}],
+ result = {res, integer}},
+
+ %% This command is also implemented in mod_admin_contrib
+ #ejabberd_commands{name = set_nickname, tags = [vcard],
+ desc = "Define user nickname",
+ longdesc = "Set/updated nickname in the user Vcard. "
+ "Other informations are unchanged.",
+ module = ?MODULE, function = set_nickname,
+ args = [{user, string}, {server, string}, {nick,string}],
+ result = {res, integer}},
+
+ %% This command is also implemented in mod_admin_contrib
+ #ejabberd_commands{name = add_rosteritem, tags = [roster],
+ desc = "Add an entry in a user's roster",
+ longdesc = "Some arguments are:\n"
+ " - jid: the JabberID of the user you would "
+ "like to add in user roster on the server.\n"
+ " - subs: the state of the roster item subscription.\n\n"
+ "The allowed values of the 'subs' argument are: both, to, from or none.\n"
+ " - none: presence packets are not sent between parties.\n"
+ " - both: presence packets are sent in both direction.\n"
+ " - to: the user sees the presence of the given JID.\n"
+ " - from: the JID specified sees the user presence.\n\n"
+ "ejabberd sends to the user's connected client both the roster item and the presence."
+ "Don't forget that roster items should keep symmetric: "
+ "when adding a roster item for a user, "
+ "you have to do the symmetric roster item addition.\n\n",
+ module = ?MODULE, function = add_rosteritem,
+ args = [{user, string}, {server, string}, {jid, string},
+ {group, string}, {nick, string}, {subs, string}],
+ result = {res, integer}},
+
+ %% This command is also implemented in mod_admin_contrib
+ #ejabberd_commands{name = delete_rosteritem, tags = [roster],
+ desc = "Remove a roster item from the user's roster",
+ longdesc = "Roster items should be kept symmetric: "
+ "when removing a roster item for a user you have to do "
+ "the symmetric roster item removal. \n\n"
+ "ejabberd sends to the user's connected client both the roster item removel and the presence unsubscription."
+ "This mechanism bypass the standard roster approval "
+ "addition mechanism and should only be used for server "
+ "administration or server integration purpose.",
+ module = ?MODULE, function = delete_rosteritem,
+ args = [{user, string}, {server, string}, {jid, string}],
+ result = {res, integer}},
+
+ #ejabberd_commands{name = add_rosteritem_groups, tags = [roster],
+ desc = "Add new groups in an existing roster item",
+ longdesc = "The argument Groups must be a string with group names separated by the character ;",
+ module = ?MODULE, function = add_rosteritem_groups,
+ args = [{user, string}, {server, string}, {jid, string},
+ {groups, string}, {push, string}],
+ result = {res, integer}},
+
+ #ejabberd_commands{name = del_rosteritem_groups, tags = [roster],
+ desc = "Delete groups in an existing roster item",
+ longdesc = "The argument Groups must be a string with group names separated by the character ;",
+ module = ?MODULE, function = del_rosteritem_groups,
+ args = [{user, string}, {server, string}, {jid, string},
+ {groups, string}, {push, string}],
+ result = {res, integer}},
+
+ #ejabberd_commands{name = modify_rosteritem_groups, tags = [roster],
+ desc = "Modify the groups of an existing roster item",
+ longdesc = "The argument Groups must be a string with group names separated by the character ;",
+ module = ?MODULE, function = modify_rosteritem_groups,
+ args = [{user, string}, {server, string}, {jid, string},
+ {groups, string}, {subs, string}, {push, string}],
+ result = {res, integer}},
+
+ #ejabberd_commands{name = link_contacts, tags = [roster],
+ desc = "Add a symmetrical entry in two users roster",
+ longdesc = "jid1 is the JabberID of the user1 you would "
+ "like to add in user2 roster on the server.\n"
+ "nick1 is the nick of user1.\n"
+ "group1 is the group name when adding user1 to user2 roster.\n"
+ "jid2 is the JabberID of the user2 you would like to "
+ "add in user1 roster on the server.\n"
+ "nick2 is the nick of user2.\n"
+ "group2 is the group name when adding user2 to user1 roster.\n\n"
+ "This mechanism bypasses the standard roster approval "
+ "addition mechanism "
+ "and should only be userd for server administration or "
+ "server integration purpose.",
+ module = ?MODULE, function = link_contacts,
+ args = [{jid1, string}, {nick1, string}, {group1, string}, {jid2, string}, {nick2, string}, {group2, string}],
+ result = {res, integer}},
+
+ #ejabberd_commands{name = unlink_contacts, tags = [roster],
+ desc = "Remove a symmetrical entry in two users roster",
+ longdesc = "jid1 is the JabberID of the user1.\n"
+ "jid2 is the JabberID of the user2.\n\n"
+ "This mechanism bypass the standard roster approval "
+ "addition mechanism "
+ "and should only be used for server administration or "
+ "server integration purpose.",
+ module = ?MODULE, function = unlink_contacts,
+ args = [{jid1, string}, {jid2, string}],
+ result = {res, integer}},
+
+ %% TODO: test
+ %% This command is not supported by ejabberdctl
+ #ejabberd_commands{name = add_contacts, tags = [roster],
+ desc = "Call add_rosteritem with subscription \"both\" "
+ "for a given list of contacts",
+ module = ?MODULE, function = add_contacts,
+ args = [{user, string},
+ {server, string},
+ {contacts, {list,
+ {contact, {tuple, [
+ {jid, string},
+ {group, string},
+ {nick, string}
+ ]}}
+ }}
+ ],
+ result = {res, integer}},
+ %% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, add_contacts, [{struct,
+ %% [{user, "badlop"},
+ %% {server, "localhost"},
+ %% {contacts, {array, [{struct, [
+ %% {contact, {array, [{struct, [
+ %% {group, "Friends"},
+ %% {jid, "tom@localhost"},
+ %% {nick, "Tom"}
+ %% ]}]}}
+ %% ]}]}}
+ %% ]
+ %% }]}).
+
+ %% TODO: test
+ %% This command is not supported by ejabberdctl
+ #ejabberd_commands{name = remove_contacts, tags = [roster],
+ desc = "Call del_rosteritem for a list of contacts",
+ module = ?MODULE, function = remove_contacts,
+ args = [{user, string},
+ {server, string},
+ {contacts, {list,
+ {jid, string}
+ }}
+ ],
+ result = {res, integer}},
+ %% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, remove_contacts, [{struct,
+ %% [{user, "badlop"},
+ %% {server, "localhost"},
+ %% {contacts, {array, [{struct, [
+ %% {jid, "tom@localhost"}
+ %% ]}]}}
+ %% ]
+ %% }]}).
+
+ %% TODO: test
+ %% This command is not supported by ejabberdctl
+ #ejabberd_commands{name = check_users_registration, tags = [roster],
+ desc = "List registration status for a list of users",
+ module = ?MODULE, function = check_users_registration,
+ args = [{users, {list,
+ {auser, {tuple, [
+ {user, string},
+ {server, string}
+ ]}}
+ }}
+ ],
+ result = {users, {list,
+ {auser, {tuple, [
+ {user, string},
+ {server, string},
+ {status, integer}
+ ]}}
+ }}},
+ %% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, check_users_registration, [{struct,
+ %% [{users, {array, [{struct, [
+ %% {auser, {array, [{struct, [
+ %% {user, "badlop"},
+ %% {server, "localhost"}
+ %% ]}]}}
+ %% ]}]}}]
+ %% }]}).
+
+ %% This command is also implemented in mod_admin_contrib
+ #ejabberd_commands{name = get_roster, tags = [roster],
+ desc = "Retrieve the roster for a given user",
+ longdesc = "Returns a list of the contacts in a user "
+ "roster.\n\n"
+ "Also returns the state of the contact subscription. "
+ "Subscription can be either "
+ " \"none\", \"from\", \"to\", \"both\". "
+ "Pending can be \"in\", \"out\" or \"none\".",
+ module = ?MODULE, function = get_roster,
+ args = [{user, string}, {server, string}],
+ result = {contacts, {list, {contact, {tuple, [{jid, string}, {groups, {list, {group, string}}},
+ {nick, string}, {subscription, string}, {pending, string}]}}}}},
+
+ #ejabberd_commands{name = get_roster_with_presence, tags = [roster],
+ desc = "Retrieve the roster for a given user including "
+ "presence information",
+ longdesc = "The 'show' value contains the user presence. "
+ "It can take limited values:\n"
+ " - available\n"
+ " - chat (Free for chat)\n"
+ " - away\n"
+ " - dnd (Do not disturb)\n"
+ " - xa (Not available, extended away)\n"
+ " - unavailable (Not connected)\n\n"
+ "'status' is a free text defined by the user client.\n\n"
+ "Also returns the state of the contact subscription. "
+ "Subscription can be either "
+ "\"none\", \"from\", \"to\", \"both\". "
+ "Pending can be \"in\", \"out\" or \"none\".\n\n"
+ "Note: If user is connected several times, only keep the"
+ " resource with the highest non-negative priority.",
+ module = ?MODULE, function = get_roster_with_presence,
+ args = [{user, string}, {server, string}],
+ result = {contacts, {list, {contact, {tuple, [{jid, string}, {resource, string}, {group, string}, {nick, string}, {subscription, string}, {pending, string}, {show, string}, {status, string}]}}}}},
+
+ #ejabberd_commands{name = get_presence, tags = [session],
+ desc = "Retrieve the resource with highest priority, "
+ "and its presence (show and status message) for a given "
+ "user.",
+ longdesc = "The 'jid' value contains the user jid with "
+ "resource.\n"
+ "The 'show' value contains the user presence flag. "
+ "It can take limited values:\n"
+ " - available\n"
+ " - chat (Free for chat)\n"
+ " - away\n"
+ " - dnd (Do not disturb)\n"
+ " - xa (Not available, extended away)\n"
+ " - unavailable (Not connected)\n\n"
+ "'status' is a free text defined by the user client.",
+ module = ?MODULE, function = get_presence,
+ args = [{user, string}, {server, string}],
+ result = {presence, {tuple, [{jid, string},
+ {show, string},
+ {status, string}]}}},
+
+ #ejabberd_commands{name = get_resources, tags = [session],
+ desc = "Get all available resources for a given user",
+ module = ?MODULE, function = get_resources,
+ args = [{user, string}, {server, string}],
+ result = {resources, {list, {resource, string}}}},
+
+ %% PubSub
+ #ejabberd_commands{name = update_status, tags = [pubsub],
+ desc = "Update the status on behalf of a user",
+ longdesc =
+ "jid: the JabberID of the user. Example: user@domain.\n\n"
+ "node: the reference of the node to publish on.\n"
+ "Example: http://process-one.net/protocol/availability\n\n"
+ "itemid: the reference of the item (in our case profile ID).\n\n"
+ "payload: the payload of the publish operation in XML.\n"
+ "The string has to be properly escaped to comply with XML formalism of XML RPC.",
+ module = ?MODULE, function = update_status,
+ args = [{jid, string}, {node, string}, {itemid, string}, {payload, string}],
+ result = {res, string}},
+
+ #ejabberd_commands{name = delete_status, tags = [pubsub],
+ desc = "Delete the status on behalf of a user",
+ longdesc =
+ "jid: the JabberID of the user. Example: user@domain.\n\n"
+ "node: the reference of the node to publish on.\n"
+ "Example: http://process-one.net/protocol/availability\n\n"
+ "itemid: the reference of the item (in our case profile ID).",
+ module = ?MODULE, function = delete_status,
+ args = [{jid, string}, {node, string}, {itemid, string}],
+ result = {res, string}},
+
+ #ejabberd_commands{name = transport_register, tags = [transports],
+ desc = "Register a user in a transport",
+ module = ?MODULE, function = transport_register,
+ args = [{host, string}, {transport, string},
+ {jidstring, string}, {username, string}, {password, string}],
+ result = {res, string}},
+
+ %% Similar to mod_admin_contrib send_message which sends a headline
+ #ejabberd_commands{name = send_chat, tags = [stanza],
+ desc = "Send chat message to a given user",
+ module = ?MODULE, function = send_chat,
+ args = [{from, string}, {to, string}, {body, string}],
+ result = {res, integer}},
+
+ #ejabberd_commands{name = send_message, tags = [stanza],
+ desc = "Send normal message to a given user",
+ module = ?MODULE, function = send_message,
+ args = [{from, string}, {to, string},
+ {subject, string}, {body, string}],
+ result = {res, integer}},
+
+ #ejabberd_commands{name = send_stanza, tags = [stanza],
+ desc = "Send stanza to a given user",
+ longdesc = "If Stanza contains a \"from\" field, "
+ "then it overrides the passed from argument."
+ "If Stanza contains a \"to\" field, then it overrides "
+ "the passed to argument.",
+ module = ?MODULE, function = send_stanza,
+ args = [{user, string}, {server, string},
+ {stanza, string}],
+ result = {res, integer}}
+ ].
+
+
+%%%
+%%% Erlang
+%%%
+
+restart_module(ModuleString, Host) ->
+ Module = list_to_atom(ModuleString),
+ List = gen_mod:loaded_modules_with_opts(Host),
+ Opts = case lists:keysearch(Module,1, List) of
+ {value, {_, O}} -> O;
+ _ -> []
+ end,
+ gen_mod:stop_module(Host, Module),
+ code:delete(Module),
+ code:purge(Module),
+ gen_mod:start_module(Host, Module, Opts),
+ ok.
+
+
+%%%
+%%% Accounts
+%%%
+
+create_account(U, S, P) ->
+ case ejabberd_auth:try_register(U, S, P) of
+ {atomic, ok} ->
+ 0;
+ {atomic, exists} ->
+ 409;
+ _ ->
+ 1
+ end.
+
+delete_account(U, S) ->
+ Fun = fun() -> ejabberd_auth:remove_user(U, S) end,
+ user_action(U, S, Fun, ok).
+
+change_password(U, S, P) ->
+ Fun = fun() -> ejabberd_auth:set_password(U, S, P) end,
+ user_action(U, S, Fun, ok).
+
+rename_account(U, S, NU, NS) ->
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ case ejabberd_auth:get_password(U, S) of
+ false ->
+ 1;
+ Password ->
+ case ejabberd_auth:try_register(NU, NS, Password) of
+ {atomic, ok} ->
+ OldJID = jlib:jid_to_string({U, S, ""}),
+ NewJID = jlib:jid_to_string({NU, NS, ""}),
+ Roster = get_roster2(U, S),
+ lists:foreach(fun(#roster{jid={RU, RS, RE}, name=Nick, groups=Groups}) ->
+ NewGroup = extract_group(Groups),
+ {NewNick, Group} = case lists:filter(fun(#roster{jid={PU, PS, _}}) ->
+ (PU == U) and (PS == S)
+ end, get_roster2(RU, RS)) of
+ [#roster{name=OldNick, groups=OldGroups}|_] -> {OldNick, extract_group(OldGroups)};
+ [] -> {NU, []}
+ end,
+ JIDStr = jlib:jid_to_string({RU, RS, RE}),
+ link_contacts2(NewJID, NewNick, NewGroup, JIDStr, Nick, Group, true),
+ unlink_contacts2(OldJID, JIDStr, true)
+ end, Roster),
+ ejabberd_auth:remove_user(U, S),
+ 0;
+ {atomic, exists} ->
+ 409;
+ _ ->
+ 1
+ end
+ end;
+ false ->
+ 404
+ end.
+
+
+%%%
+%%% Sessions
+%%%
+
+get_presence(U, S) ->
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ {Resource, Show, Status} = get_presence2(U, S),
+ FullJID = case Resource of
+ [] ->
+ lists:flatten([U,"@",S]);
+ _ ->
+ lists:flatten([U,"@",S,"/",Resource])
+ end,
+ {FullJID, Show, Status};
+ false ->
+ 404
+ end.
+
+get_resources(U, S) ->
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ get_resources2(U, S);
+ false ->
+ 404
+ end.
+
+
+%%%
+%%% Vcard
+%%%
+
+set_nickname(U, S, N) ->
+ Fun = fun() -> case mod_vcard:process_sm_iq(
+ {jid, U, S, "", U, S, ""},
+ {jid, U, S, "", U, S, ""},
+ {iq, "", set, "", "en",
+ {xmlelement, "vCard",
+ [{"xmlns", "vcard-temp"}], [
+ {xmlelement, "NICKNAME", [], [{xmlcdata, N}]}
+ ]
+ }}) of
+ {iq, [], result, [], _, []} -> ok;
+ _ -> error
+ end
+ end,
+ user_action(U, S, Fun, ok).
+
+
+%%%
+%%% Roster
+%%%
+
+add_rosteritem(U, S, JID, G, N, Subs) ->
+ add_rosteritem(U, S, JID, G, N, Subs, true).
+
+add_rosteritem(U, S, JID, G, N, Subs, Push) ->
+ Fun = fun() -> add_rosteritem2(U, S, JID, N, G, Subs, Push) end,
+ user_action(U, S, Fun, {atomic, ok}).
+
+link_contacts(JID1, Nick1, Group1, JID2, Nick2, Group2) ->
+ link_contacts(JID1, Nick1, Group1, JID2, Nick2, Group2, true).
+
+link_contacts(JID1, Nick1, Group1, JID2, Nick2, Group2, Push) ->
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case {ejabberd_auth:is_user_exists(U1, S1), ejabberd_auth:is_user_exists(U2, S2)} of
+ {true, true} ->
+ case link_contacts2(JID1, Nick1, Group1, JID2, Nick2, Group2, Push) of
+ {atomic, ok} ->
+ 0;
+ _ ->
+ 1
+ end;
+ _ ->
+ 404
+ end.
+
+delete_rosteritem(U, S, JID) ->
+ Fun = fun() -> del_rosteritem(U, S, JID) end,
+ user_action(U, S, Fun, {atomic, ok}).
+
+unlink_contacts(JID1, JID2) ->
+ unlink_contacts(JID1, JID2, true).
+
+unlink_contacts(JID1, JID2, Push) ->
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case {ejabberd_auth:is_user_exists(U1, S1), ejabberd_auth:is_user_exists(U2, S2)} of
+ {true, true} ->
+ case unlink_contacts2(JID1, JID2, Push) of
+ {atomic, ok} ->
+ 0;
+ _ ->
+ 1
+ end;
+ _ ->
+ 404
+ end.
+
+get_roster(U, S) ->
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ format_roster(get_roster2(U, S));
+ false ->
+ 404
+ end.
+
+get_roster_with_presence(U, S) ->
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ format_roster_with_presence(get_roster2(U, S));
+ false ->
+ 404
+ end.
+
+add_contacts(U, S, Contacts) ->
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ JID1 = jlib:jid_to_string({U, S, ""}),
+ lists:foldl(fun({JID2, Group, Nick}, Acc) ->
+ {PU, PS, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case ejabberd_auth:is_user_exists(PU, PS) of
+ true ->
+ case link_contacts2(JID1, "", Group, JID2, Nick, Group, true) of
+ {atomic, ok} -> Acc;
+ _ -> 1
+ end;
+ false ->
+ Acc
+ end
+ end, 0, Contacts);
+ false ->
+ 404
+ end.
+
+remove_contacts(U, S, Contacts) ->
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ JID1 = jlib:jid_to_string({U, S, ""}),
+ lists:foldl(fun(JID2, Acc) ->
+ {PU, PS, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case ejabberd_auth:is_user_exists(PU, PS) of
+ true ->
+ case unlink_contacts2(JID1, JID2, true) of
+ {atomic, ok} -> Acc;
+ _ -> 1
+ end;
+ false ->
+ Acc
+ end
+ end, 0, Contacts);
+ false ->
+ 404
+ end.
+
+check_users_registration(Users) ->
+ lists:map(fun({U, S}) ->
+ Registered = case ejabberd_auth:is_user_exists(U, S) of
+ true -> 1;
+ false -> 0
+ end,
+ {U, S, Registered}
+ end, Users).
+
+%%%
+%%% Groups of Roster Item
+%%%
+
+add_rosteritem_groups(User, Server, JID, NewGroupsString, PushString) ->
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ NewGroups = string:tokens(NewGroupsString, ";"),
+ Push = list_to_atom(PushString),
+ case {ejabberd_auth:is_user_exists(U1, S1), ejabberd_auth:is_user_exists(User, Server)} of
+ {true, true} ->
+ case add_rosteritem_groups2(User, Server, JID, NewGroups, Push) of
+ ok ->
+ 0;
+ Error ->
+ ?INFO_MSG("Error found: ~n~p", [Error]),
+ 1
+ end;
+ _ ->
+ 404
+ end.
+
+del_rosteritem_groups(User, Server, JID, NewGroupsString, PushString) ->
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ NewGroups = string:tokens(NewGroupsString, ";"),
+ Push = list_to_atom(PushString),
+ case {ejabberd_auth:is_user_exists(U1, S1), ejabberd_auth:is_user_exists(User, Server)} of
+ {true, true} ->
+ case del_rosteritem_groups2(User, Server, JID, NewGroups, Push) of
+ ok ->
+ 0;
+ Error ->
+ ?INFO_MSG("Error found: ~n~p", [Error]),
+ 1
+ end;
+ _ ->
+ 404
+ end.
+
+modify_rosteritem_groups(User, Server, JID, NewGroupsString, SubsString, PushString) ->
+ Nick = "", %% That information will not be used, anyway
+ Subs = list_to_atom(SubsString),
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ NewGroups = string:tokens(NewGroupsString, ";"),
+ Push = list_to_atom(PushString),
+ case ejabberd_auth:is_user_exists(User, Server) of
+ true ->
+ case modify_rosteritem_groups2(User, Server, JID, NewGroups, Push, Nick, Subs) of
+ ok ->
+ 0;
+ Error ->
+ ?INFO_MSG("Error found: ~n~p", [Error]),
+ 1
+ end;
+ _ ->
+ 404
+ end.
+
+add_rosteritem_groups2(User, Server, JID, NewGroups, Push) ->
+ GroupsFun =
+ fun(Groups) ->
+ lists:usort(NewGroups ++ Groups)
+ end,
+ change_rosteritem_group(User, Server, JID, GroupsFun, Push).
+
+del_rosteritem_groups2(User, Server, JID, NewGroups, Push) ->
+ GroupsFun =
+ fun(Groups) ->
+ Groups -- NewGroups
+ end,
+ change_rosteritem_group(User, Server, JID, GroupsFun, Push).
+
+modify_rosteritem_groups2(User, Server, JID2, NewGroups, Push, Nick, Subs) when NewGroups == [] ->
+ JID1 = jlib:jid_to_string(jlib:make_jid(User, Server, "")),
+ case unlink_contacts(JID1, JID2) of
+ {atomic, ok} ->
+ ok;
+ Error ->
+ Error
+ end;
+modify_rosteritem_groups2(User, Server, JID, NewGroups, Push, Nick, Subs) ->
+ GroupsFun =
+ fun(_Groups) ->
+ NewGroups
+ end,
+ change_rosteritem_group(User, Server, JID, GroupsFun, Push, NewGroups, Nick, Subs).
+
+change_rosteritem_group(User, Server, JID, GroupsFun, Push) ->
+ change_rosteritem_group(User, Server, JID, GroupsFun, Push, [], "", "both").
+
+change_rosteritem_group(User, Server, JID, GroupsFun, Push, NewGroups, Nick, Subs) ->
+ {RU, RS, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ LJID = {RU,RS,[]},
+ LUser = jlib:nodeprep(User),
+ LServer = jlib:nameprep(Server),
+ Result =
+ case roster_backend(LServer) of
+ mnesia ->
+ mnesia:transaction(
+ fun() ->
+ case mnesia:read({roster, {LUser, LServer, LJID}}) of
+ [#roster{} = Roster] ->
+ NewGroups2 = GroupsFun(Roster#roster.groups),
+ NewRoster = Roster#roster{groups = NewGroups2},
+ mnesia:write(NewRoster),
+ {ok, NewRoster#roster.name,
+ NewRoster#roster.subscription,
+ NewGroups2};
+ _ ->
+ not_in_roster
+ end
+ end);
+ odbc ->
+ ejabberd_odbc:sql_transaction(
+ LServer,
+ fun() ->
+ Username = ejabberd_odbc:escape(User),
+ SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)),
+ case ejabberd_odbc:sql_query_t(
+ ["select nick, subscription from rosterusers "
+ " where username='", Username, "' "
+ " and jid='", SJID, "';"]) of
+ {selected, ["nick", "subscription"],
+ [{Name, SSubscription}]} ->
+ Subscription =
+ case SSubscription of
+ "B" -> both;
+ "T" -> to;
+ "F" -> from;
+ _ -> none
+ end,
+ Groups =
+ case odbc_queries:get_roster_groups(
+ LServer, Username, SJID) of
+ {selected, ["grp"], JGrps}
+ when is_list(JGrps) ->
+ [JGrp || {JGrp} <- JGrps];
+ _ ->
+ []
+ end,
+ NewGroups2 = GroupsFun(Groups),
+ ejabberd_odbc:sql_query_t(
+ ["delete from rostergroups "
+ " where username='", Username, "' "
+ " and jid='", SJID, "';"]),
+ lists:foreach(
+ fun(Group) ->
+ ejabberd_odbc:sql_query_t(
+ ["insert into rostergroups("
+ " username, jid, grp) "
+ " values ('", Username, "',"
+ "'", SJID, "',"
+ "'", ejabberd_odbc:escape(Group), "');"])
+ end,
+ NewGroups2),
+ {ok, Name, Subscription, NewGroups2};
+ _ ->
+ not_in_roster
+ end
+ end);
+ none ->
+ %% Apollo change: force roster push anyway with success
+ {atomic, {ok, Nick, Subs, NewGroups}}
+ end,
+ case {Result, Push} of
+ {{atomic, {ok, Name, Subscription, NewGroups3}}, true} ->
+ roster_push(User, Server, JID,
+ Name, atom_to_list(Subscription), NewGroups3),
+ ok;
+ {{atomic, {ok, _Name, _Subscription, _NewGroups3}}, false} -> ok;
+ {{atomic, not_in_roster}, _} -> not_in_roster;
+ Error -> {error, Error}
+ end.
+
+%%%
+%%% PubSub
+%%%
+
+update_status(JidString, NodeString, Itemid, PayloadString) ->
+ Publisher = jlib:string_to_jid(JidString),
+ Host = jlib:jid_tolower(jlib:jid_remove_resource(Publisher)),
+ ServerHost = Publisher#jid.lserver,
+ Node = mod_pubsub_on:string_to_node(NodeString),
+ Payload = [xml_stream:parse_element(PayloadString)],
+ ?DEBUG("PayloadString: ~n~p~nPayload elements: ~n~p", [PayloadString, Payload]),
+ case mod_pubsub_on:publish_item_nothook(Host, ServerHost, Node, Publisher, Itemid, Payload) of
+ {result, _} ->
+ "OK";
+ {error, {xmlelement, _, _, _} = XmlEl} ->
+ "ERROR: " ++ xml:element_to_string(XmlEl);
+ {error, ErrorAtom} when is_atom(ErrorAtom) ->
+ "ERROR: " ++ atom_to_list(ErrorAtom);
+ {error, ErrorString} when is_list(ErrorString) ->
+ "ERROR: " ++ ErrorString
+ end.
+
+delete_status(JidString, NodeString, Itemid) ->
+ Publisher = jlib:string_to_jid(JidString),
+ Host = jlib:jid_tolower(jlib:jid_remove_resource(Publisher)),
+ Node = mod_pubsub_on:string_to_node(NodeString),
+ case mod_pubsub_on:delete_item_nothook(Host, Node, Publisher, Itemid, true) of
+ {result, _} ->
+ "OK";
+ {error, {xmlelement, _, _, _} = XmlEl} ->
+ "ERROR: " ++ xml:element_to_string(XmlEl);
+ {error, ErrorAtom} when is_atom(ErrorAtom) ->
+ "ERROR: " ++ atom_to_list(ErrorAtom);
+ {error, ErrorString} when is_list(ErrorString) ->
+ "ERROR: " ++ ErrorString
+ end.
+
+transport_register(Host, TransportString, JIDString, Username, Password) ->
+ TransportAtom = list_to_atom(TransportString),
+ case {lists:member(Host, ?MYHOSTS), jlib:string_to_jid(JIDString)} of
+ {true, JID} when is_record(JID, jid) ->
+ case catch gen_transport:register(Host, TransportAtom, JIDString,
+ Username, Password) of
+ ok ->
+ "OK";
+ {error, Reason} ->
+ "ERROR: " ++ atom_to_list(Reason);
+ {'EXIT', {timeout,_}} ->
+ "ERROR: timed_out";
+ {'EXIT', _} ->
+ "ERROR: unexpected_error"
+ end;
+ {false, _} ->
+ "ERROR: unknown_host";
+ _ ->
+ "ERROR: bad_jid"
+ end.
+
+%%%
+%%% Stanza
+%%%
+
+send_chat(FromJID, ToJID, Msg) ->
+ From = jlib:string_to_jid(FromJID),
+ To = jlib:string_to_jid(ToJID),
+ Stanza = {xmlelement, "message", [{"type", "chat"}],
+ [{xmlelement, "body", [], [{xmlcdata, Msg}]}]},
+ ejabberd_router:route(From, To, Stanza),
+ 0.
+
+send_message(FromJID, ToJID, Sub, Msg) ->
+ From = jlib:string_to_jid(FromJID),
+ To = jlib:string_to_jid(ToJID),
+ Stanza = {xmlelement, "message", [{"type", "normal"}],
+ [{xmlelement, "subject", [], [{xmlcdata, Sub}]},
+ {xmlelement, "body", [], [{xmlcdata, Msg}]}]},
+ ejabberd_router:route(From, To, Stanza),
+ 0.
+
+send_stanza(FromJID, ToJID, StanzaStr) ->
+ case xml_stream:parse_element(StanzaStr) of
+ {error, _} ->
+ 1;
+ Stanza ->
+ {xmlelement, _, Attrs, _} = Stanza,
+ From = jlib:string_to_jid(proplists:get_value("from", Attrs, FromJID)),
+ To = jlib:string_to_jid(proplists:get_value("to", Attrs, ToJID)),
+ ejabberd_router:route(From, To, Stanza),
+ 0
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% Internal functions
+
+%% -----------------------------
+%% Internal roster handling
+%% -----------------------------
+
+get_roster2(User, Server) ->
+ LUser = jlib:nodeprep(User),
+ LServer = jlib:nameprep(Server),
+ case roster_backend(LServer) of
+ mnesia -> mod_roster:get_user_roster([], {LUser, LServer});
+ odbc -> mod_roster_odbc:get_user_roster([], {LUser, LServer})
+ end.
+
+add_rosteritem2(User, Server, JID, Nick, Group, Subscription, Push) ->
+ {RU, RS, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ LJID = {RU,RS,[]},
+ Groups = case Group of
+ [] -> [];
+ _ -> [Group]
+ end,
+ Roster = #roster{
+ usj = {User,Server,LJID},
+ us = {User,Server},
+ jid = LJID,
+ name = Nick,
+ ask = none,
+ subscription = list_to_atom(Subscription),
+ groups = Groups},
+ Result =
+ case roster_backend(Server) of
+ mnesia ->
+ mnesia:transaction(fun() ->
+ case mnesia:read({roster,{User,Server,LJID}}) of
+ [#roster{subscription=both}] ->
+ already_added;
+ _ ->
+ mnesia:write(Roster)
+ end
+ end);
+ odbc ->
+ %% MREMOND: TODO: check if already_added
+ case ejabberd_odbc:sql_transaction(Server,
+ fun() ->
+ Username = ejabberd_odbc:escape(User),
+ SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)),
+ case ejabberd_odbc:sql_query_t(
+ ["select username from rosterusers "
+ " where username='", Username, "' "
+ " and jid='", SJID,
+ "' and subscription = 'B';"]) of
+ {selected, ["username"],[]} ->
+ ItemVals = record_to_string(Roster),
+ ItemGroups = groups_to_string(Roster),
+ ejabberd_odbc:sql_query_t(
+ odbc_queries:update_roster_sql(
+ Username, SJID, ItemVals, ItemGroups));
+ _ ->
+ already_added
+ end
+ end) of
+ {atomic, already_added} -> {atomic, already_added};
+ {atomic, _} -> {atomic, ok};
+ Error -> Error
+ end;
+ none ->
+ %% If no known mod_roster is enabled, still let the code to proceed
+ {atomic, ok}
+ end,
+ case {Result, Push} of
+ {{atomic, already_added}, _} -> ok; %% No need for roster push
+ {{atomic, ok}, true} -> roster_push(User, Server, JID, Nick, Subscription, Groups);
+ {{atomic, ok}, false} -> ok;
+ _ -> error
+ end,
+ Result.
+
+del_rosteritem(User, Server, JID) ->
+ del_rosteritem(User, Server, JID, true).
+
+del_rosteritem(User, Server, JID, Push) ->
+ {RU, RS, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ LJID = {RU,RS,[]},
+ Result = case roster_backend(Server) of
+ mnesia ->
+ mnesia:transaction(fun() ->
+ mnesia:delete({roster, {User,Server,LJID}})
+ end);
+ odbc ->
+ case ejabberd_odbc:sql_transaction(Server, fun() ->
+ Username = ejabberd_odbc:escape(User),
+ SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)),
+ odbc_queries:del_roster(Server, Username, SJID)
+ end) of
+ {atomic, _} -> {atomic, ok};
+ Error -> Error
+ end;
+ none ->
+ %% If no known mod_roster is enabled, still let the code to proceed
+ {atomic, ok}
+ end,
+ case {Result, Push} of
+ {{atomic, ok}, true} -> roster_push(User, Server, JID, "", "remove", []);
+ {{atomic, ok}, false} -> ok;
+ _ -> error
+ end,
+ Result.
+
+link_contacts2(JID1, Nick1, Group1, JID2, Nick2, Group2, Push) ->
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case add_rosteritem2(U1, S1, JID2, Nick2, Group1, "both", Push) of
+ {atomic, ok} -> add_rosteritem2(U2, S2, JID1, Nick1, Group2, "both", Push);
+ Error -> Error
+ end.
+
+unlink_contacts2(JID1, JID2, Push) ->
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case del_rosteritem(U1, S1, JID2, Push) of
+ {atomic, ok} -> del_rosteritem(U2, S2, JID1, Push);
+ Error -> Error
+ end.
+
+roster_push(User, Server, JID, Nick, Subscription, Groups) ->
+ LJID = jlib:make_jid(User, Server, ""),
+ TJID = jlib:string_to_jid(JID),
+ {TU, TS, _} = jlib:jid_tolower(TJID),
+ Presence =
+ {xmlelement, "presence",
+ [{"type",
+ case Subscription of
+ "remove" -> "unsubscribed";
+ "none" -> "unsubscribe";
+ "both" -> "subscribed";
+ _ -> "subscribe"
+ end}], []},
+ ItemAttrs =
+ case Nick of
+ "" -> [{"jid", JID}, {"subscription", Subscription}];
+ _ -> [{"jid", JID}, {"name", Nick}, {"subscription", Subscription}]
+ end,
+ ItemGroups =
+ lists:map(fun(G) ->
+ {xmlelement, "group", [], [{xmlcdata, G}]}
+ end, Groups),
+ Result =
+ jlib:iq_to_xml(
+ #iq{type = set, xmlns = ?NS_ROSTER, id = "push",
+ lang = "langxmlrpc-en",
+ sub_el = [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}],
+ [{xmlelement, "item", ItemAttrs, ItemGroups}]}]}),
+ %% ejabberd_router:route(TJID, LJID, Presence),
+ %% ejabberd_router:route(LJID, LJID, Result),
+ lists:foreach(
+ fun(Resource) ->
+ UJID = jlib:make_jid(User, Server, Resource),
+ ejabberd_router:route(TJID, UJID, Presence),
+ ejabberd_router:route(UJID, UJID, Result),
+ case Subscription of
+ "remove" -> none;
+ _ ->
+ lists:foreach(
+ fun(TR) ->
+ ejabberd_router:route(
+ jlib:make_jid(TU, TS, TR), UJID,
+ {xmlelement, "presence", [], []})
+ end, get_resources(TU, TS))
+ end
+ end, [R || R <- get_resources(User, Server)]).
+
+roster_backend(Server) ->
+ Modules = gen_mod:loaded_modules(Server),
+ Mnesia = lists:member(mod_roster, Modules),
+ Odbc = lists:member(mod_roster_odbc, Modules),
+ if Mnesia -> mnesia;
+ true ->
+ if Odbc -> odbc;
+ true -> none
+ end
+ end.
+
+record_to_string(#roster{us = {User, _Server},
+ jid = JID,
+ name = Name,
+ subscription = Subscription,
+ ask = Ask,
+ askmessage = AskMessage}) ->
+ Username = ejabberd_odbc:escape(User),
+ SJID = ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))),
+ Nick = ejabberd_odbc:escape(Name),
+ SSubscription = case Subscription of
+ both -> "B";
+ to -> "T";
+ from -> "F";
+ none -> "N"
+ end,
+ SAsk = case Ask of
+ subscribe -> "S";
+ unsubscribe -> "U";
+ both -> "B";
+ out -> "O";
+ in -> "I";
+ none -> "N"
+ end,
+ SAskMessage = ejabberd_odbc:escape(AskMessage),
+ ["'", Username, "',"
+ "'", SJID, "',"
+ "'", Nick, "',"
+ "'", SSubscription, "',"
+ "'", SAsk, "',"
+ "'", SAskMessage, "',"
+ "'N', '', 'item'"].
+
+groups_to_string(#roster{us = {User, _Server},
+ jid = JID,
+ groups = Groups}) ->
+ Username = ejabberd_odbc:escape(User),
+ SJID = ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))),
+ %% Empty groups do not need to be converted to string to be inserted in
+ %% the database
+ lists:foldl(fun([], Acc) -> Acc;
+ (Group, Acc) ->
+ String = ["'", Username, "',"
+ "'", SJID, "',"
+ "'", ejabberd_odbc:escape(Group), "'"],
+ [String|Acc]
+ end, [], Groups).
+
+%% Format roster items as a list of:
+%% [{struct, [{jid, "test@localhost"},{group, "Friends"},{nick, "Nicktest"}]}]
+format_roster([]) ->
+ [];
+format_roster(Items) ->
+ format_roster(Items, []).
+format_roster([], Structs) ->
+ Structs;
+format_roster([#roster{jid=JID, name=Nick, groups=Group,
+ subscription=Subs, ask=Ask}|Items], Structs) ->
+ {User,Server,_Resource} = JID,
+ Struct = {lists:flatten([User,"@",Server]),
+ Group,
+ Nick,
+ atom_to_list(Subs),
+ atom_to_list(Ask)
+ },
+ format_roster(Items, [Struct|Structs]).
+
+%% Note: If user is connected several times, only keep the resource with the
+%% highest non-negative priority
+format_roster_with_presence([]) ->
+ [];
+format_roster_with_presence(Items) ->
+ format_roster_with_presence(Items, []).
+format_roster_with_presence([], Structs) ->
+ Structs;
+format_roster_with_presence([#roster{jid=JID, name=Nick, groups=Group,
+ subscription=Subs, ask=Ask}|Items], Structs) ->
+ {User,Server,_R} = JID,
+ Presence = case Subs of
+ both -> get_presence2(User, Server);
+ from -> get_presence2(User, Server);
+ _Other -> {"", "unavailable", ""}
+ end,
+ {Resource, Show, Status} =
+ case Presence of
+ {_R, "invisible", _S} -> {"", "unavailable", ""};
+ _Status -> Presence
+ end,
+ Struct = {lists:flatten([User,"@",Server]),
+ Resource,
+ extract_group(Group),
+ Nick,
+ atom_to_list(Subs),
+ atom_to_list(Ask),
+ Show,
+ Status
+ },
+ format_roster_with_presence(Items, [Struct|Structs]).
+
+extract_group([]) -> [];
+%extract_group([Group|_Groups]) -> Group.
+extract_group(Groups) -> string:join(Groups, ";").
+
+extract_groups([]) -> [];
+%extract_groups([Group|_Groups]) -> Group.
+extract_groups(Groups) -> {list, Groups}.
+
+%% -----------------------------
+%% Internal session handling
+%% -----------------------------
+
+%% This is inspired from ejabberd_sm.erl
+get_presence2(User, Server) ->
+ case get_sessions(User, Server) of
+ [] ->
+ {"", "unavailable", ""};
+ Ss ->
+ Session = hd(Ss),
+ if Session#session.priority >= 0 ->
+ Pid = element(2, Session#session.sid),
+ %{_User, _Resource, Show, Status} = rpc:call(node(Pid), ejabberd_c2s, get_presence, [Pid]),
+ {_User, Resource, Show, Status} = ejabberd_c2s:get_presence(Pid),
+ {Resource, Show, Status};
+ true ->
+ {"", "unavailable", ""}
+ end
+ end.
+
+get_resources2(User, Server) ->
+ lists:map(fun(S) -> element(3, S#session.usr)
+ end, get_sessions(User, Server)).
+
+get_sessions(User, Server) ->
+ US = {jlib:nodeprep(User), jlib:nameprep(Server)},
+ Node = ejabberd_cluster:get_node(US),
+ case catch rpc:call(Node, mnesia, dirty_index_read,
+ [session, US, #session.us], 5000) of
+ Result when is_list(Result), Result /= [] ->
+ lists:reverse(lists:keysort(#session.priority, clean_session_list(Result)));
+ _ ->
+ []
+ end.
+
+clean_session_list(Ss) ->
+ clean_session_list(lists:keysort(#session.usr, Ss), []).
+
+clean_session_list([], Res) ->
+ Res;
+clean_session_list([S], Res) ->
+ [S | Res];
+clean_session_list([S1, S2 | Rest], Res) ->
+ if
+ S1#session.usr == S2#session.usr ->
+ if
+ S1#session.sid > S2#session.sid ->
+ clean_session_list([S1 | Rest], Res);
+ true ->
+ clean_session_list([S2 | Rest], Res)
+ end;
+ true ->
+ clean_session_list([S2 | Rest], [S1 | Res])
+ end.
+
+
+%% -----------------------------
+%% Internal function pattern
+%% -----------------------------
+
+user_action(User, Server, Fun, OK) ->
+ case ejabberd_auth:is_user_exists(User, Server) of
+ true ->
+ case catch Fun() of
+ OK ->
+ 0;
+ _ ->
+ 1
+ end;
+ false ->
+ 404
+ end.
diff --git a/src/mod_antiflood.erl b/src/mod_antiflood.erl
new file mode 100644
index 000000000..aca0737eb
--- /dev/null
+++ b/src/mod_antiflood.erl
@@ -0,0 +1,186 @@
+%%%-------------------------------------------------------------------
+%%% File : mod_antiflood.erl
+%%% Author : Christophe Romain <cromain@process-one.net>
+%%% Description :
+%%% Created : 12 Sep 2008 by Christophe Romain <cromain@process-one.net>
+%%%
+%%% ejabberd, Copyright (C) 2002-2010 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+-module(mod_antiflood).
+-author('cromain@process-one.net').
+
+-behaviour(gen_mod).
+
+%% API
+-export([start/2, stop/1]).
+-export([handler/4]).
+
+-include("ejabberd.hrl").
+
+%%====================================================================
+%% API
+%%====================================================================
+start(Host, Opts) ->
+ floodcheck:start_link(),
+ lists:foreach(fun
+ ({listeners, Info, Op, Val}) ->
+ lists:foreach(fun({Name, Pid}) ->
+ Id = {Name, Info},
+ floodcheck:monitor(Id, Pid, Info, {Op, Val}, {?MODULE, handler})
+ end, supervised_processes(listeners));
+ ({Module, Info, Op, Val}) ->
+ case module_pid(Host, Module) of
+ Pid when is_pid(Pid) ->
+ Id = {Host, Module, Info},
+ floodcheck:monitor(Id, Pid, Info, {Op, Val}, {?MODULE, handler});
+ Error ->
+ ?INFO_MSG("can not monitor ~s (~p)", [Module, Error])
+ end;
+ (Arg) ->
+ ?INFO_MSG("invalid argument: ~p", [Arg])
+ end, Opts).
+
+stop(Host) ->
+ MList = gen_mod:loaded_modules_with_opts(Host),
+ case lists:keysearch(?MODULE, 1, MList) of
+ {value, {?MODULE, Opts}} ->
+ lists:foreach(fun
+ ({Type, Info, _, _}) ->
+ case supervised_processes(Type) of
+ [] ->
+ Id = {Host, Type, Info},
+ floodcheck:demonitor(Id);
+ Childs ->
+ lists:foreach(fun({Name, _}) ->
+ Id = {Name, Info},
+ floodcheck:demonitor(Id)
+ end, Childs)
+ end;
+ (_) ->
+ ok
+ end, Opts);
+ false ->
+ ok
+ end,
+ case floodcheck:check() of
+ [] -> floodcheck:stop(), ok;
+ _ -> ok
+ end.
+
+handler({Host, Module, Info}, Pid, Info, Value) ->
+ ?WARNING_MSG("Flood alert on Process ~p (~s on ~s): ~s=~p", [Pid, Module, Host, Info, Value]),
+ restart_module(Host, Module),
+ remonitor({Host, Module, Info});
+handler({Name, Info}, Pid, Info, Value) ->
+ ?WARNING_MSG("Flood alert on Process ~p (~s): ~s=~p", [Pid, Name, Info, Value]),
+ kill_process(Name, Pid),
+ remonitor({Name, Info});
+handler(Id, Pid, Info, Value) ->
+ ?WARNING_MSG("Flood alert on Process ~p (~s): ~s=~p~nUnknown id, alert ignored", [Pid, Id, Info, Value]).
+
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
+
+process_pid(Name) -> whereis(Name).
+server_pid(Host, Name) -> process_pid(gen_mod:get_module_proc(Host, Name)).
+
+module_pid(Host, mod_caps) -> server_pid(Host, ejabberd_mod_caps);
+module_pid(Host, mod_ip_blacklist) -> server_pid(Host, mod_ip_blacklist);
+module_pid(Host, mod_offline) -> server_pid(Host, ejabberd_offline);
+module_pid(Host, mod_offline_odbc) -> server_pid(Host, ejabberd_offline);
+module_pid(Host, mod_vcard) -> server_pid(Host, ejabberd_mod_vcard);
+module_pid(Host, mod_vcard_odbc) -> server_pid(Host, ejabberd_mod_vcard);
+module_pid(Host, mod_vcard_ldap) -> server_pid(Host, ejabberd_mod_vcard_ldap);
+module_pid(Host, mod_irc) -> server_pid(Host, ejabberd_mod_irc);
+module_pid(Host, mod_muc) -> server_pid(Host, ejabberd_mod_muc);
+module_pid(Host, mod_muc_log) -> server_pid(Host, ejabberd_mod_muc_log);
+module_pid(Host, mod_proxy65) -> server_pid(Host, ejabberd_mod_proxy65);
+module_pid(Host, mod_proxy65_service) -> server_pid(Host, ejabberd_mod_proxy65_service);
+module_pid(Host, mod_proxy65_sm) -> server_pid(Host, ejabberd_mod_proxy65_sm);
+module_pid(Host, mod_pubsub) -> server_pid(Host, ejabberd_mod_pubsub);
+module_pid(_, _) -> unsupported.
+
+supervised_processes(listeners) ->
+ {links, Links} = process_info(whereis(ejabberd_listeners), links),
+ lists:map(fun(Pid) ->
+ {dictionary, Dict} = process_info(Pid, dictionary),
+ {_, _, [Port|_]} = proplists:get_value('$initial_call', Dict),
+ Name = list_to_atom("listener_"++integer_to_list(Port)),
+ {Name, Pid}
+ end, Links);
+supervised_processes(_) -> [].
+
+remonitor({Host, Module, Info}) ->
+ MList = gen_mod:loaded_modules_with_opts(Host),
+ case lists:keysearch(?MODULE, 1, MList) of
+ {value, {?MODULE, Opts}} ->
+ lists:foreach(fun
+ ({M, I, Op, Val}) when M =:= Module, I =:= Info ->
+ case module_pid(Host, Module) of
+ Pid when is_pid(Pid) ->
+ Id = {Host, Module, Info},
+ floodcheck:monitor(Id, Pid, Info, {Op, Val}, {?MODULE, handler});
+ Error ->
+ ?INFO_MSG("can not monitor ~s (~p)", [Module, Error])
+ end;
+ (_) ->
+ ok
+ end, Opts);
+ _ ->
+ ok
+ end;
+remonitor({Name, Info}) ->
+ [Host|_] = ejabberd_config:get_global_option(hosts),
+ MList = gen_mod:loaded_modules_with_opts(Host),
+ case lists:keysearch(?MODULE, 1, MList) of
+ {value, {?MODULE, Opts}} ->
+ lists:foreach(fun
+ ({Type, I, Op, Val}) when I =:= Info ->
+ lists:foreach(fun
+ ({N, Pid}) when N =:= Name ->
+ Id = {Name, Info},
+ floodcheck:monitor(Id, Pid, Info, {Op, Val}, {?MODULE, handler});
+ (_) ->
+ ok
+ end, supervised_processes(Type));
+ (_) ->
+ ok
+ end, Opts);
+ _ ->
+ ok
+ end;
+remonitor(Id) ->
+ ?INFO_MSG("can not monitor ~s", [Id]).
+
+restart_module(Host, Module) ->
+ MList = gen_mod:loaded_modules_with_opts(Host),
+ case lists:keysearch(Module, 1, MList) of
+ {value, {Module, Opts}} ->
+ ?WARNING_MSG("restarting module ~s on ~s", [Module, Host]),
+ gen_mod:stop_module(Host, Module),
+ gen_mod:start_module(Host, Module, Opts);
+ _ ->
+ not_running
+ end.
+
+kill_process(Name, Pid) ->
+ ?WARNING_MSG("killing process ~s(~p)", [Name, Pid]),
+ exit(Pid, kill).
diff --git a/src/mod_autofilter.erl b/src/mod_autofilter.erl
new file mode 100644
index 000000000..7fa45fb6f
--- /dev/null
+++ b/src/mod_autofilter.erl
@@ -0,0 +1,129 @@
+%%% ====================================================================
+%%% This software is copyright 2006-2010, ProcessOne.
+%%%
+%%% mod_autofilter
+%%%
+%%% @copyright 2006-2010 ProcessOne
+%%% @author Christophe Romain <christophe.romain@process-one.net>
+%%% [http://www.process-one.net/]
+%%% @version {@vsn}, {@date} {@time}
+%%% @end
+%%% ====================================================================
+
+
+-module(mod_autofilter).
+-author('christophe.romain@process-one.net').
+
+-behaviour(gen_mod).
+
+% module functions
+-export([start/2,stop/1,is_loaded/0]).
+-export([offline_message/3,filter_packet/1,close_session/2,close_session/3]).
+-export([deny/2,allow/2,denied/0,listed/0,purge/1]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("licence.hrl").
+
+-record(autofilter, {key, timestamp=0, count=1, drop=false, reason}).
+
+start(Host, Opts) ->
+ case ?IS_VALID of
+ true ->
+ mnesia:create_table(autofilter, [
+ {disc_copies, [node()]}, {type, set},
+ {attributes, record_info(fields, autofilter)} ]),
+ ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, offline_message, 10),
+ ejabberd_hooks:add(filter_packet, ?MODULE, filter_packet, 10),
+ ejabberd_hooks:add(sm_remove_connection_hook, Host, ?MODULE, close_session, 10),
+ case gen_mod:get_opt(purge_freq, Opts, 0) of %% purge_freq in minutes
+ 0 ->
+ no_purge;
+ Freq ->
+ Keep = gen_mod:get_opt(keep, Opts, 10), %% keep in minutes
+ timer:apply_interval(Freq*60000, ?MODULE, purge, [Keep*60])
+ end,
+ start;
+ false ->
+ not_started
+ end.
+
+stop(Host) ->
+ ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, offline_message, 10),
+ ejabberd_hooks:delete(filter_packet, ?MODULE, filter_packet, 10),
+ ejabberd_hooks:delete(sm_remove_connection_hook, Host, ?MODULE, close_session, 10),
+ stop.
+
+is_loaded() ->
+ ok.
+
+purge(Keep) ->
+ ?INFO_MSG("autofilter purge",[]),
+ {T1, T2, _} = now(),
+ Older = T1*1000000+T2-Keep,
+ lists:foreach(fun(Key) ->
+ mnesia:dirty_delete({autofilter, Key})
+ end, mnesia:dirty_select(autofilter, [{#autofilter{key = '$1', drop = false, timestamp = '$2', _ = '_'}, [{'<', '$2', Older}], ['$1']}])),
+ ok.
+
+deny(User, Server) ->
+ Key = {User, Server},
+ Record = case mnesia:dirty_read({autofilter, Key}) of
+ [R] -> R;
+ _ -> #autofilter{key=Key}
+ end,
+ ?INFO_MSG("autofilter: messages from ~s@~s will be droped~n", [User, Server]),
+ mnesia:dirty_write(Record#autofilter{drop=true}).
+
+allow(User, Server) ->
+ Key = {User, Server},
+ ?INFO_MSG("autofilter: messages from ~s@~s are accepted~n", [User, Server]),
+ mnesia:dirty_delete({autofilter, Key}).
+
+denied() ->
+ mnesia:dirty_select(autofilter, [{#autofilter{key = '$1', drop = true, reason = '$2', _ = '_'}, [], [['$1','$2']]}]).
+listed() ->
+ mnesia:dirty_select(autofilter, [{#autofilter{key = '$1', drop = false, reason = '$2', _ = '_'}, [], [['$1','$2']]}]).
+
+offline_message({jid, [], _, [], [], _, []}, _To, _Packet) ->
+ ok;
+offline_message(From, _To, _Packet) ->
+ {User, Server, _} = jlib:jid_tolower(From),
+ Key = {User, Server},
+ {T1, T2, _} = now(),
+ T = T1*1000000+T2,
+ Record = case mnesia:dirty_read({autofilter, Key}) of
+ [#autofilter{timestamp=O, count=C}=R] ->
+ D = T-O,
+ if
+ D > 3600 -> % this is usefull only of purge_freq is not set
+ R#autofilter{timestamp=T, count=1};
+ ((C/D) > 1/10) and (C > 90) ->
+ ?INFO_MSG("autofilter: messages from ~s@~s will be droped~n", [User, Server]),
+ R#autofilter{drop=true, reason=offline_flood};
+ true ->
+ R#autofilter{count=C+1}
+ end;
+ _ ->
+ #autofilter{key=Key, timestamp=T, count=1, drop=false, reason=offline_flood}
+ end,
+ mnesia:dirty_write(Record),
+ ok.
+
+filter_packet({From, To, {xmlelement, "message", _, _}=Packet}) ->
+ {User, Server, _} = jlib:jid_tolower(From),
+ case mnesia:dirty_read({autofilter, {User, Server}}) of
+ [#autofilter{drop=true}] -> drop;
+ _ -> {From, To, Packet}
+ end;
+filter_packet(OK) ->
+ OK.
+
+close_session(SID, JID) ->
+ close_session(SID, JID, []).
+close_session(_SID, {jid, _, _, _, User, Server, _}, _Info) ->
+ % this allows user, except for blocked ones
+ lists:foreach(fun(#autofilter{key=Key}) ->
+ mnesia:dirty_delete({autofilter, Key})
+ end, mnesia:dirty_match_object(#autofilter{key={User, Server}, drop = false, _ = '_'})),
+ ok.
diff --git a/src/mod_c2s_debug.erl b/src/mod_c2s_debug.erl
new file mode 100644
index 000000000..41c6e9dcd
--- /dev/null
+++ b/src/mod_c2s_debug.erl
@@ -0,0 +1,205 @@
+%% Usage:
+%% In config file:
+%% {mod_c2s_debug, [{logdir, "/tmp/xmpplogs"}]},
+%% It is possible to limit to a specific jid with option:
+%% {users, ["test@localhost"]}
+%% Warning: Only works with a single JID for now.
+%%
+%% Start from Erlang shell:
+%% mod_c2s_debug:start("localhost", []).
+%% mod_c2s_debug:stop("localhost").
+%%
+%% Warning: Only one module for the debug handler can be defined.
+-module(mod_c2s_debug).
+-author('mremond@process-one.net').
+
+-behaviour(gen_mod).
+-behavior(gen_server).
+
+-export([start/2, start_link/2, stop/1,
+ debug_start/3, debug_stop/2, log_packet/4, log_packet/5]).
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("ejabberd_c2s.hrl").
+
+-record(modstate, {host, logdir, pid, iodevice, user}).
+-record(clientinfo, {pid, jid, auth_module, ip}).
+
+-define(SUPERVISOR, ejabberd_sup).
+-define(PROCNAME, c2s_debug).
+
+%%====================================================================
+%% gen_mod callbacks
+%%====================================================================
+start(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ Spec = {Proc, {?MODULE, start_link, [Host, Opts]},
+ transient, 2000, worker, [?MODULE]},
+ supervisor:start_child(?SUPERVISOR, Spec).
+
+stop(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:call(Proc, stop),
+ supervisor:delete_child(?SUPERVISOR, Proc).
+
+start_link(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+%%====================================================================
+%% Hooks
+%%====================================================================
+
+%% Debug handled by another module... Do nothing:
+debug_start(_Status, Pid, C2SState) ->
+ Host = C2SState#state.server,
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+
+ JID = jlib:jid_to_string(C2SState#state.jid),
+ AuthModule = C2SState#state.auth_module,
+ IP = C2SState#state.ip,
+ ClientInfo = #clientinfo{pid = Pid, jid = JID, auth_module = AuthModule, ip = IP},
+
+ gen_server:call(Proc, {debug_start, ClientInfo}).
+
+debug_stop(Pid, C2SState) ->
+ Host = C2SState#state.server,
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:cast(Proc, {debug_stop, Pid}).
+
+log_packet(false, _FromJID, _ToJID, _Packet) ->
+ ok;
+log_packet(true, FromJID, ToJID, Packet) ->
+ Host = FromJID#jid.lserver,
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:cast(Proc, {addlog, {"Send", FromJID, ToJID, Packet}}).
+log_packet(false, _JID, _FromJID, _ToJID, _Packet) ->
+ ok;
+log_packet(true, JID, FromJID, ToJID, Packet) ->
+ Host = JID#jid.lserver,
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:cast(Proc, {addlog, {"Receive", FromJID, ToJID, Packet}}).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+init([Host, Opts]) ->
+ ?INFO_MSG("Starting c2s debug module for: ~p", [Host]),
+ MyHost = gen_mod:get_opt_host(Host, Opts, "c2s_debug.@HOST@"),
+ ejabberd_hooks:add(c2s_debug_start_hook, Host,
+ ?MODULE, debug_start, 50),
+ ejabberd_hooks:add(c2s_debug_stop_hook, Host,
+ ?MODULE, debug_stop, 50),
+ ejabberd_hooks:add(user_send_packet, Host, ?MODULE, log_packet, 50),
+ ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, log_packet, 50),
+
+ Logdir = gen_mod:get_opt(logdir, Opts, "/tmp/xmpplogs/"),
+ %% TODO: We currently support only one user. Support multiple users
+ SJID = case gen_mod:get_opt(users, Opts, undefined) of
+ undefined ->
+ undefined;
+ [User1|_] ->
+ User1
+ end,
+ make_dir_rec(Logdir),
+ {ok, #modstate{host = MyHost, logdir = Logdir, user = jlib:string_to_jid(SJID)}}.
+
+terminate(_Reason, #modstate{host = Host}) ->
+ ?INFO_MSG("Stopping c2s debug module for: ~s", [Host]),
+ ejabberd_hooks:delete(c2s_debug_start_hook, Host,
+ ?MODULE, debug_start, 50),
+ ejabberd_hooks:delete(c2s_debug_stop_hook, Host,
+ ?MODULE, debug_stop, 50),
+ ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, log_packet, 50).
+
+%% No specific user: Select the first new user to connect
+handle_call({debug_start, ClientInfo}, _From, #modstate{pid=undefined, user=undefined} = State) ->
+ Pid = ClientInfo#clientinfo.pid,
+ ?INFO_MSG("Debug started for PID:~p", [Pid]),
+
+ JID = ClientInfo#clientinfo.jid,
+ AuthModule = ClientInfo#clientinfo.auth_module,
+ IP = ClientInfo#clientinfo.ip,
+
+ {ok, IOD} = file:open(filename(State#modstate.logdir), [append]),
+ Line = io_lib:format("~s - Session open~nJID: ~s~nAuthModule: ~p~nIP: ~p~n",
+ [timestamp(), JID, AuthModule, IP]),
+ file:write(IOD, Line),
+
+ {reply, true, State#modstate{pid = Pid, iodevice = IOD}};
+%% Targeting a specific user
+handle_call({debug_start, ClientInfo}, _From, #modstate{pid=undefined, user=JID} = State) ->
+ ClientJID = ClientInfo#clientinfo.jid,
+ case jlib:jid_remove_resource(jlib:string_to_jid(ClientJID)) of
+ JID ->
+ Pid = ClientInfo#clientinfo.pid,
+ ?INFO_MSG("Debug started for PID:~p", [Pid]),
+ AuthModule = ClientInfo#clientinfo.auth_module,
+ IP = ClientInfo#clientinfo.ip,
+
+ {ok, IOD} = file:open(filename(State#modstate.logdir), [append]),
+ Line = io_lib:format("~s - Session open~nJID: ~s~nAuthModule: ~p~nIP: ~p~n",
+ [timestamp(), ClientJID, AuthModule, IP]),
+ file:write(IOD, Line),
+ {reply, true, State#modstate{pid = Pid, iodevice = IOD}};
+ _ ->
+ {reply, false, State}
+ end;
+handle_call({debug_start, _ClientInfo}, _From, State) ->
+ {reply, false, State};
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Req, _From, State) ->
+ {reply, {error, badarg}, State}.
+
+handle_cast({addlog, _}, #modstate{iodevice=undefined} = State) ->
+ {noreply, State};
+handle_cast({addlog, {Direction, FromJID, ToJID, Packet}}, #modstate{iodevice=IOD} = State) ->
+ LogEntry = io_lib:format("=====~n~s - ~s~nFrom: ~s~nTo: ~s~n~s~n", [timestamp(), Direction,
+ jlib:jid_to_string(FromJID),
+ jlib:jid_to_string(ToJID),
+ xml:element_to_string(Packet)]),
+ file:write(IOD, LogEntry),
+ {noreply, State};
+handle_cast({debug_stop, Pid}, #modstate{pid=Pid, iodevice=IOD} = State) ->
+ Line = io_lib:format("=====~n~s - Session closed~n",
+ [timestamp()]),
+ file:write(IOD, Line),
+
+ file:close(IOD),
+ {noreply, State#modstate{pid = undefined, iodevice=undefined}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%% Generate filename
+filename(LogDir) ->
+ Filename = lists:flatten(timestamp()) ++ "-c2s.log",
+ filename:join([LogDir, Filename]).
+
+%% Generate timestamp
+timestamp() ->
+ {Y,Mo,D} = erlang:date(),
+ {H,Mi,S} = erlang:time(),
+ io_lib:format("~4.4.0w~2.2.0w~2.2.0w-~2.2.0w~2.2.0w~2.2.0w", [Y,Mo,D,H,Mi,S]).
+
+%% Create dir recusively
+make_dir_rec(Dir) ->
+ case file:read_file_info(Dir) of
+ {ok, _} ->
+ ok;
+ {error, enoent} ->
+ DirS = filename:split(Dir),
+ DirR = lists:sublist(DirS, length(DirS)-1),
+ make_dir_rec(filename:join(DirR)),
+ file:make_dir(Dir)
+ end.
diff --git a/src/mod_caps.erl b/src/mod_caps.erl
index ba934e758..c56ebc2eb 100644
--- a/src/mod_caps.erl
+++ b/src/mod_caps.erl
@@ -53,8 +53,8 @@
]).
%% hook handlers
--export([user_send_packet/3,
- user_receive_packet/4,
+-export([user_send_packet/4,
+ user_receive_packet/5,
c2s_presence_in/2,
c2s_broadcast_recipients/5]).
@@ -146,7 +146,8 @@ read_caps([], Result) ->
%%====================================================================
%% Hooks
%%====================================================================
-user_send_packet(#jid{luser = User, lserver = Server} = From,
+user_send_packet(_DebugFlag,
+ #jid{luser = User, lserver = Server} = From,
#jid{luser = User, lserver = Server, lresource = ""},
{xmlelement, "presence", Attrs, Els}) ->
Type = xml:get_attr_s("type", Attrs),
@@ -160,13 +161,18 @@ user_send_packet(#jid{luser = User, lserver = Server} = From,
true ->
ok
end;
-user_send_packet(_From, _To, _Packet) ->
+user_send_packet(_DebugFlag, _From, _To, _Packet) ->
ok.
-user_receive_packet(#jid{lserver = Server}, From, _To,
+user_receive_packet(_DebugFlag,
+ #jid{lserver = Server}, From, _To,
{xmlelement, "presence", Attrs, Els}) ->
Type = xml:get_attr_s("type", Attrs),
- if Type == ""; Type == "available" ->
+ IsRemote = not lists:member(From#jid.lserver, ?MYHOSTS),
+ %% Local users presence caps are already handled by user_send_packet.
+ %% Otherwise we could send multiple request when broadcasting presence
+ %% to every local subscriber.
+ if IsRemote and ((Type == "") or (Type == "available")) ->
case read_caps(Els) of
nothing ->
ok;
@@ -176,7 +182,7 @@ user_receive_packet(#jid{lserver = Server}, From, _To,
true ->
ok
end;
-user_receive_packet(_JID, _From, _To, _Packet) ->
+user_receive_packet(_DebugFlag, _JID, _From, _To, _Packet) ->
ok.
caps_stream_features(Acc, MyHost) ->
diff --git a/src/mod_filter.erl b/src/mod_filter.erl
new file mode 100644
index 000000000..ecc3446d6
--- /dev/null
+++ b/src/mod_filter.erl
@@ -0,0 +1,370 @@
+%%% ====================================================================
+%%% This software is copyright 2006-2010, ProcessOne.
+%%%
+%%% mod_filter
+%%% allow message filtering using regexp on message body
+%%% THIS MODULE NEEDS A PATCHED ERLANG VM AGAINST
+%%% THE PCRE PATCH AND NEEDS LIBPCRE INSTALLED
+%%% ejabberd MUST USE THAT PATCHED ERLANG VM
+%%% BUT, if patch is not available, mod_filter uses re.beam module
+%%% instead, with speed degradation.
+%%%
+%%% @copyright 2006-2010 ProcessOne
+%%% @author Christophe Romain <christophe.romain@process-one.net>
+%%% [http://www.process-one.net/]
+%%% @version {@vsn}, {@date} {@time}
+%%% @end
+%%% ====================================================================
+
+-module(mod_filter).
+-author('christophe.romain@process-one.net').
+-vsn('$Id: mod_filter.erl 972 2010-10-14 21:53:56Z jpcarlino $').
+
+-behaviour(gen_mod).
+
+% module functions
+-export([start/2,stop/1,init/2,update/2,is_loaded/0,loop/5]).
+-export([add_regexp/4,add_regexp/3,del_regexp/3,del_regexp/2]).
+-export([purge_logs/0,purge_regexps/1,reload/1]).
+-export([logged/0,logged/1,rules/0]).
+-export([process_local_iq/3]).
+
+% handled ejabberd hooks
+-export([filter_packet/1]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("licence.hrl").
+
+-record(filter_rule, {id, type="filter", regexp, binre}).
+-record(filter_log, {date, from, to, message}).
+
+-define(TIMEOUT, 5000). % deliver message anyway if filter does not respond after 5s
+-define(PROCNAME(VH), list_to_atom(VH++"_message_filter")).
+-define(NS_FILTER, "p1:iq:filter").
+
+-define(ALLHOSTS, "all hosts"). %% must be sync with filter.erl
+
+start(Host, Opts) ->
+ case ?IS_VALID of
+ true ->
+ mnesia:create_table(filter_rule, [
+ {disc_copies, [node()]}, {type, set},
+ {attributes, record_info(fields, filter_rule)} ]),
+ mnesia:create_table(filter_log, [
+ {disc_only_copies, [node()]}, {type, bag},
+ {attributes, record_info(fields, filter_log)} ]),
+ %% this force the last code to be used
+ case whereis(?PROCNAME(Host)) of
+ undefined ->
+ ok;
+ _ ->
+ ejabberd_hooks:delete(filter_packet, ?MODULE, filter_packet, 10),
+ gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_FILTER),
+ ?PROCNAME(Host) ! quit
+ end,
+ ejabberd_hooks:add(filter_packet, ?MODULE, filter_packet, 10),
+ gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_FILTER,
+ ?MODULE, process_local_iq, one_queue),
+ case whereis(?PROCNAME(Host)) of
+ undefined -> register(?PROCNAME(Host), spawn(?MODULE, init, [Host, Opts]));
+ _ -> ok
+ end,
+ %% start the all_alias handler
+ case whereis(?PROCNAME(?ALLHOSTS)) of
+ undefined -> init_all_hosts_handler();
+ _ -> ok
+ end,
+ start;
+ false ->
+ not_started
+ end.
+
+stop(Host) ->
+ ejabberd_hooks:delete(filter_packet, ?MODULE, filter_packet, 10),
+ gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_FILTER),
+ exit(whereis(?PROCNAME(Host)), kill),
+ {wait, ?PROCNAME(Host)}.
+
+% this is used by team_leader to check code presence
+is_loaded() ->
+ ok.
+
+% this loads rules and return {Types, BinRegExps}
+% RegExps are text regexp list
+% Types are regexp type list
+% both list share the same ordered
+load_rules(Host) ->
+ Rules = mnesia:dirty_match_object(#filter_rule{id={'_', Host}, _ = '_'})
+ ++ mnesia:dirty_match_object(#filter_rule{id={'_', ?ALLHOSTS}, _ = '_'}),
+ lists:map(fun({filter_rule, _, Type, _, BinRegExp}) -> {Type, BinRegExp} end, Rules).
+ %lists:unzip(Config).
+
+% this call init or reset local rules reading database
+init(Host, Opts) ->
+ Rules = load_rules(Host),
+ Scope = gen_mod:get_opt(scope, Opts, message),
+ Pattern = gen_mod:get_opt(pattern, Opts, ""),
+ ?MODULE:loop(Host, Opts, Rules, Scope, Pattern).
+
+init_all_hosts_handler() ->
+ register(?PROCNAME(?ALLHOSTS), spawn(?MODULE, loop, [?ALLHOSTS, [], [], none, []])).
+
+% this call reset local rules reading database
+% and tell other nodes to reset rules as well
+update(?ALLHOSTS, _Opts) ->
+ lists:foreach(fun(Host) ->
+ lists:foreach(fun(Node) ->
+ catch rpc:call(Node,mod_filter,reload,[Host])
+ end, mnesia:system_info(running_db_nodes))
+ end, ejabberd_config:get_global_option(hosts)),
+ ?MODULE:loop(?ALLHOSTS, [], [], none, []);
+update(Host, Opts) ->
+ % tell other nodes to update filter
+ lists:foreach(fun(Node) ->
+ catch rpc:call(Node,mod_filter,reload,[Host])
+ end, mnesia:system_info(running_db_nodes)--[node()]),
+ % update rules
+ init(Host, Opts).
+
+loop(Host, Opts, Rules, Scope, Pattern) ->
+ receive
+ {add, Id, RegExp} ->
+ try
+ [BinRegExp] = tlre:compile([RegExp]),
+ ?INFO_MSG("Adding new filter rule with regexp=~p", [RegExp]),
+ mnesia:dirty_write(#filter_rule{id={Id, Host}, regexp=RegExp, binre=BinRegExp}),
+ ?MODULE:update(Host, Opts)
+ catch
+ Class:Reason ->
+ ?INFO_MSG("~p can't add filter rule with regexp=~p for id=~p. Reason: ~p", [Class, RegExp, Id, Reason]),
+ loop(Host, Opts, Rules, Scope, Pattern)
+ end;
+ {add, Id, RegExp, Type} ->
+ try
+ [BinRegExp] = tlre:compile([RegExp]),
+ ?INFO_MSG("Adding new filter rule with regexp=~p", [RegExp]),
+ mnesia:dirty_write(#filter_rule{id={Id, Host}, regexp=RegExp, binre=BinRegExp, type=Type}),
+ ?MODULE:update(Host, Opts)
+ catch
+ Class:Reason ->
+ ?INFO_MSG("~p can't add filter rule with regexp=~p for id=~p with type=~p. Reason: ~p", [Class, RegExp, Id, Type, Reason]),
+ loop(Host, Opts, Rules, Scope, Pattern)
+ end;
+ {del, Id} ->
+ RulesToRemove = mnesia:dirty_match_object(#filter_rule{id={Id, Host}, _='_'}),
+ lists:foreach(fun(Rule) ->
+ mnesia:dirty_delete_object(Rule)
+ end, RulesToRemove),
+ ?MODULE:update(Host, Opts);
+ {del, Id, RegExp} ->
+ RulesToRemove = mnesia:dirty_match_object(#filter_rule{id={Id, Host}, regexp=RegExp, _='_'}),
+ lists:foreach(fun(Rule) ->
+ mnesia:dirty_delete_object(Rule)
+ end, RulesToRemove),
+ ?MODULE:update(Host, Opts);
+ {match, From, String} ->
+ From ! {match, string_filter(String, Rules, Scope, Pattern)},
+ ?MODULE:loop(Host, Opts, Rules, Scope, Pattern);
+ reload ->
+ ?MODULE:init(Host, Opts);
+ quit ->
+ unregister(?PROCNAME(Host)),
+ ok
+ end.
+
+string_filter(String, Rules, Scope, Pattern) ->
+ lists:foldl(fun
+ (_, {Pass, []}) -> {Pass, []};
+ ({Type, RegExp}, {Pass, NewString}) -> string_filter(NewString, Pass, RegExp, Type, Scope, Pattern)
+ end, {"pass", String}, Rules).
+string_filter(String, Pass, RegExp, Type, Scope, Pattern) ->
+ case tlre:grep(String, [RegExp]) of
+ [no_match] ->
+ {Pass, String};
+ [{S1, S2, _}] ->
+ case Scope of
+ word ->
+ Start = string:sub_string(String, 1, S1),
+ StringTail = string:sub_string(String, S2+1, length(String)),
+ NewPass = pass_rule(Pass, Type),
+ {LastPass, End} = string_filter(StringTail, NewPass, RegExp, Type, Scope, Pattern),
+ NewString = case Type of
+ "log" -> lists:append([string:sub_string(String, 1, S2), End]);
+ _ -> lists:append([Start, Pattern, End])
+ end,
+ {LastPass, NewString};
+ _ ->
+ NewString = case Type of
+ "log" -> String;
+ _ -> []
+ end,
+ {pass_rule(Pass, Type), NewString}
+ end
+ end.
+
+pass_rule("pass", New) -> New;
+pass_rule("log", "log") -> "log";
+pass_rule("log", "log and filter") -> "log and filter";
+pass_rule("log", "filter") -> "log and filter";
+pass_rule("filter", "log") -> "log and filter";
+pass_rule("filter", "log and filter") -> "log and filter";
+pass_rule("filter", "filter") -> "filter";
+pass_rule("log and filter", _) -> "log and filter".
+
+add_regexp(VH, Id, RegExp) ->
+ ?PROCNAME(VH) ! {add, Id, RegExp},
+ ok.
+
+add_regexp(VH, Id, RegExp, Type) ->
+ ?PROCNAME(VH) ! {add, Id, RegExp, Type},
+ ok.
+
+del_regexp(VH, Id) ->
+ ?PROCNAME(VH) ! {del, Id},
+ ok.
+
+del_regexp(VH, Id, RegExp) ->
+ ?PROCNAME(VH) ! {del, Id, RegExp},
+ ok.
+
+reload(VH) ->
+ ?PROCNAME(VH) ! reload,
+ ok.
+
+purge_logs() ->
+ mnesia:dirty_delete_object(#filter_log{_='_'}).
+
+%purge_regexps() ->
+% mnesia:dirty_delete_object(#filter_rule{_='_'}),
+% reload().
+
+purge_regexps(VH) ->
+ mnesia:dirty_delete_object(#filter_rule{id={'_', VH}, _='_'}),
+ reload(VH).
+
+rules() ->
+ lists:map(fun(#filter_rule{id={Label, VH}, type=Type, regexp=Regexp}) ->
+ {VH, Label, Type, Regexp}
+ end, mnesia:dirty_match_object(#filter_rule{_='_'})).
+
+logged() ->
+ lists:reverse(lists:map(fun(#filter_log{date=Date, from=From, to=To, message=Msg}) ->
+ {Date, jlib:jid_to_string(From), jlib:jid_to_string(To), Msg}
+ end, mnesia:dirty_match_object(#filter_log{_='_'}))).
+
+logged(Limit) ->
+ List = mnesia:dirty_match_object(#filter_log{_='_'}),
+ Len = length(List),
+ FinalList = if
+ Len < Limit -> List;
+ true -> lists:nthtail(Len-Limit, List)
+ end,
+ lists:reverse(lists:map(fun(#filter_log{date=Date, from=From, to=To, message=Msg}) ->
+ {Date, jlib:jid_to_string(From), jlib:jid_to_string(To), Msg}
+ end, FinalList)).
+
+%% filter_packet can receive drop if a previous filter already dropped
+%% the packet
+filter_packet(drop) -> drop;
+filter_packet({From, To, Packet}) ->
+ case Packet of
+ {xmlelement, "message", MsgAttrs, Els} ->
+ case lists:keysearch("body", 2, Els) of
+ {value, {xmlelement, "body", BodyAttrs, Data}} ->
+ NewData = lists:foldl(fun
+ ({xmlcdata, CData}, DataAcc) when is_binary(CData) ->
+ #jid{lserver = Host} = To,
+ case lists:member(Host, ejabberd_config:get_global_option(hosts)) of
+ true ->
+ Msg = binary_to_list(CData),
+ ?PROCNAME(Host) ! {match, self(), Msg},
+ receive
+ {match, {"pass", _}} ->
+ [{xmlcdata, CData}|DataAcc];
+ {match, {"log", _}} ->
+ mnesia:dirty_write(#filter_log{
+ date=erlang:localtime(),
+ from=From, to=To, message=Msg}),
+ [{xmlcdata, CData}|DataAcc];
+ {match, {"log and filter", FinalString}} ->
+ mnesia:dirty_write(#filter_log{
+ date=erlang:localtime(),
+ from=From, to=To, message=Msg}),
+ case FinalString of
+ [] -> % entire message is dropped
+ DataAcc;
+ S -> % message must be regenerated
+ [{xmlcdata, list_to_binary(S)}|DataAcc]
+ end;
+ {match, {"filter", FinalString}} ->
+ case FinalString of
+ [] -> % entire message is dropped
+ DataAcc;
+ S -> % message must be regenerated
+ [{xmlcdata, list_to_binary(S)}|DataAcc]
+ end
+ after ?TIMEOUT ->
+ [{xmlcdata, CData}|DataAcc]
+ end;
+ false ->
+ [{xmlcdata, CData}|DataAcc]
+ end;
+ (Item, DataAcc) -> %% to not filter internal messages
+ [Item|DataAcc]
+ end, [], Data),
+ case NewData of
+ [] ->
+ drop;
+ D ->
+ NewEls = lists:keyreplace("body", 2, Els, {xmlelement, "body", BodyAttrs, lists:reverse(D)}),
+ {From, To, {xmlelement, "message", MsgAttrs, NewEls}}
+ end;
+ _ ->
+ {From, To, Packet}
+ end;
+ _ ->
+ {From, To, Packet}
+ end.
+
+process_local_iq(From, #jid{lserver=VH} = _To, #iq{type = Type, sub_el = SubEl} = IQ) ->
+ case Type of
+ get ->
+ IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]};
+ set ->
+ #jid{luser = User, lserver = Server, lresource = Resource} = From,
+ case acl:match_rule(global, configure, {User, Server, Resource}) of
+ allow ->
+ case xml:get_subtag(SubEl, "add") of
+ {xmlelement, "add", AddAttrs, _} ->
+ AID = xml:get_attr_s("id", AddAttrs),
+ ARE = xml:get_attr_s("re", AddAttrs),
+ case xml:get_attr_s("type", AddAttrs) of
+ "" -> add_regexp(VH, AID, ARE);
+ ATP -> add_regexp(VH, AID, ARE, ATP)
+ end;
+ _ -> ok
+ end,
+ case xml:get_subtag(SubEl, "del") of
+ {xmlelement, "del", DelAttrs, _} ->
+ DID = xml:get_attr_s("id", DelAttrs),
+ case xml:get_attr_s("re", DelAttrs) of
+ "" -> del_regexp(VH, DID);
+ DRE -> del_regexp(VH, DID, DRE)
+ end;
+ _ -> ok
+ end,
+ case xml:get_subtag(SubEl, "dellogs") of
+ {xmlelement, "dellogs", _, _} -> purge_logs();
+ _ -> ok
+ end,
+ case xml:get_subtag(SubEl, "delrules") of
+ {xmlelement, "delrules", _, _} -> purge_regexps(VH);
+ _ -> ok
+ end,
+ IQ#iq{type = result, sub_el = []};
+ _ ->
+ IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}
+ end
+ end.
+
diff --git a/src/mod_ip_blacklist.erl b/src/mod_ip_blacklist.erl
index 15884496f..c928e9797 100644
--- a/src/mod_ip_blacklist.erl
+++ b/src/mod_ip_blacklist.erl
@@ -92,7 +92,7 @@ loop(_State) ->
%% TODO: Support comment lines starting by %
update_bl_c2s() ->
?INFO_MSG("Updating C2S Blacklist", []),
- case httpc:request(?BLC2S) of
+ case http_p1:request(?BLC2S) of
{ok, {{_Version, 200, _Reason}, _Headers, Body}} ->
IPs = string:tokens(Body,"\n"),
ets:delete_all_objects(bl_c2s),
diff --git a/src/mod_mnesia_mngt.erl b/src/mod_mnesia_mngt.erl
new file mode 100644
index 000000000..0c5ad1d36
--- /dev/null
+++ b/src/mod_mnesia_mngt.erl
@@ -0,0 +1,147 @@
+%% Usage:
+%% In config file:
+%% {mod_mnesia_mgmt, [{logdir, "/tmp/xmpplogs/"}]},
+%% From Erlang shell:
+%% mod_mnesia_mgmt:start("localhost", []).
+%% mod_mnesia_mgmt:stop("localhost").
+
+-module(mod_mnesia_mngt).
+-author('mremond@process-one.net').
+
+-behaviour(gen_mod).
+-behavior(gen_server).
+
+-export([start/2, start_link/2, stop/1]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+
+-record(modstate, {host, logdir, iodevice, timer}).
+
+-define(SUPERVISOR, ejabberd_sup).
+-define(PROCNAME, mod_mnesia_mgmt).
+
+-define(STANDARD_ACCEPT_INTERVAL, 20). %% accept maximum one new connection every 20ms
+-define(ACCEPT_INTERVAL, 200). %% This is used when Mnesia is overloaded
+-define(RATE_LIMIT_DURATION, 120000). %% Time during which the rate limitation need to be maintained
+
+%%====================================================================
+%% gen_mod callbacks
+%%====================================================================
+start(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ Spec = {Proc, {?MODULE, start_link, [Host, Opts]},
+ transient, 2000, worker, [?MODULE]},
+ supervisor:start_child(?SUPERVISOR, Spec).
+
+stop(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:call(Proc, stop),
+ supervisor:delete_child(?SUPERVISOR, Proc).
+
+start_link(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+init([Host, Opts]) ->
+ ?INFO_MSG("Starting mod_mnesia_mgmt for: ~p", [Host]),
+ ejabberd_listener:rate_limit([5222, 5223], ?STANDARD_ACCEPT_INTERVAL),
+ MyHost = gen_mod:get_opt_host(Host, Opts, "mnesia_mngt.@HOST@"),
+
+ Logdir = gen_mod:get_opt(logdir, Opts, "/tmp/xmpplogs/"),
+ make_dir_rec(Logdir),
+ {ok, IOD} = file:open(filename(Logdir), [append]),
+
+ mnesia:subscribe(system),
+
+ {ok, #modstate{host = MyHost, logdir = Logdir, iodevice = IOD}}.
+
+terminate(_Reason, #modstate{host = Host, iodevice = IOD}) ->
+ ?INFO_MSG("Stopping mod_mnesia_mgmt for: ~s", [Host]),
+ mnesia:unsubscribe(system),
+ file:close(IOD).
+
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Req, _From, State) ->
+ {reply, {error, badarg}, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info({mnesia_system_event,{mnesia_overload, {mnesia_tm, message_queue_len, Values}}},
+ #modstate{iodevice = IOD, timer = Timer} = State) ->
+ Line = io_lib:format("~s - Mnesia overload due to message queue length (~p)",
+ [timestamp(), Values]),
+ file:write(IOD, Line),
+ reset_timer(Timer),
+
+ {messages, Messages} = process_info(whereis(mnesia_tm), messages),
+ log_messages(IOD, Messages, 20),
+ {noreply, State#modstate{timer = undefined}};
+handle_info({mnesia_system_event,{mnesia_overload, Details}},
+ #modstate{iodevice = IOD, timer = Timer} = State) ->
+ Line = io_lib:format("~s - Mnesia overload: ~p",
+ [timestamp(), Details]),
+ file:write(IOD, Line),
+ reset_timer(Timer),
+ {noreply, State};
+handle_info({mnesia_system_event, _Event}, State) ->
+ %% TODO: More event to handle
+ {noreply, State};
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%% Generate filename
+filename(LogDir) ->
+ Filename = lists:flatten(timestamp()) ++ "-mnesia.log",
+ filename:join([LogDir, Filename]).
+
+%% Generate timestamp
+timestamp() ->
+ {Y,Mo,D} = erlang:date(),
+ {H,Mi,S} = erlang:time(),
+ io_lib:format("~4.4.0w~2.2.0w~2.2.0w-~2.2.0w~2.2.0w~2.2.0w", [Y,Mo,D,H,Mi,S]).
+
+%% Create dir recusively
+make_dir_rec(Dir) ->
+ case file:read_file_info(Dir) of
+ {ok, _} ->
+ ok;
+ {error, enoent} ->
+ DirS = filename:split(Dir),
+ DirR = lists:sublist(DirS, length(DirS)-1),
+ make_dir_rec(filename:join(DirR)),
+ file:make_dir(Dir)
+ end.
+
+%% Write first messages to log file
+log_messages(_IOD, _Messages, 0) ->
+ ok;
+log_messages(_IOD, [], _N) ->
+ ok;
+log_messages(IOD, [Message|Messages], N) ->
+ Line = io_lib:format("** ~w",
+ [Message]),
+ file:write(IOD, Line),
+ log_messages(IOD, Messages, N-1).
+
+reset_timer(Timer) ->
+ cancel_timer(Timer),
+ ejabberd_listener:rate_limit([5222, 5223], ?ACCEPT_INTERVAL),
+ timer:apply_after(?RATE_LIMIT_DURATION, ejabberd_listener, rate_limit, [[5222, 5223], ?STANDARD_ACCEPT_INTERVAL]).
+
+cancel_timer(undefined) ->
+ ok;
+cancel_timer(Timer) ->
+ timer:cancel(Timer).
diff --git a/src/mod_muc/mod_muc.erl b/src/mod_muc/mod_muc.erl
index 811e3c068..1f40ca0a4 100644
--- a/src/mod_muc/mod_muc.erl
+++ b/src/mod_muc/mod_muc.erl
@@ -41,6 +41,14 @@
create_room/5,
process_iq_disco_items/4,
broadcast_service_message/2,
+ register_room/3,
+ node_up/1,
+ node_down/1,
+ migrate/3,
+ get_vh_rooms/1,
+ is_broadcasted/1,
+ moderate_room_history/2,
+ persist_recent_messages/1,
can_use_nick/4]).
%% gen_server callbacks
@@ -59,6 +67,7 @@
server_host,
access,
history_size,
+ persist_history,
default_room_opts,
room_shaper}).
@@ -88,10 +97,48 @@ start(Host, Opts) ->
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
+ %% if compiled with no transient supervisor, we need to manually shutdown
+ %% the rooms to give them a chance to store persistent messages to DB
+ Rooms = shutdown_rooms(Host),
stop_supervisor(Host),
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
gen_server:call(Proc, stop),
- supervisor:delete_child(ejabberd_sup, Proc).
+ supervisor:delete_child(ejabberd_sup, Proc),
+ {wait, Rooms}. %%wait for rooms shutdown before stopping ejabberd
+
+shutdown_rooms(Host) ->
+ MyHost = gen_mod:get_module_opt_host(Host, mod_muc, "conference.@HOST@"),
+ Rooms = mnesia:dirty_select(muc_online_room,
+ [{#muc_online_room{name_host = '$1', pid = '$2'},
+ [{'==', {element, 2, '$1'}, MyHost}],
+ ['$2']}]),
+ [Pid ! 'shutdown' || Pid <- Rooms],
+ Rooms.
+
+%% Returns {RoomsPersisted, MessagesPersisted}
+persist_recent_messages(Host) ->
+ MyHost = gen_mod:get_module_opt_host(Host, mod_muc, "conference.@HOST@"),
+ Rooms = mnesia:dirty_select(muc_online_room,
+ [{#muc_online_room{name_host = '$1', pid = '$2'},
+ [{'==', {element, 2, '$1'}, MyHost}],
+ ['$2']}]),
+ lists:foldl(fun(Pid, {NRooms, Messages}) ->
+ case mod_muc_room:persist_recent_messages(Pid) of
+ {ok, {persisted, N}} -> {NRooms +1, Messages +N};
+ {ok, not_persistent} -> {NRooms, Messages}
+ end end, {0, 0}, Rooms).
+
+moderate_room_history(RoomStr, Nick) ->
+ Room = jlib:string_to_jid(RoomStr),
+ Name = Room#jid.luser,
+ Host = Room#jid.lserver,
+ case mnesia:dirty_read(muc_online_room, {Name, Host}) of
+ [] ->
+ {error, not_found};
+ [R] ->
+ Pid = R#muc_online_room.pid,
+ mod_muc_room:moderate_room_history(Pid, Nick)
+ end.
%% This function is called by a room in three situations:
%% A) The owner of the room destroyed it
@@ -109,7 +156,9 @@ room_destroyed(Host, Room, Pid, ServerHost) ->
%% Else use the passed options as defined in mod_muc_room.
create_room(Host, Name, From, Nick, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
- gen_server:call(Proc, {create, Name, From, Nick, Opts}).
+ RoomHost = gen_mod:get_module_opt_host(Host, ?MODULE, "conference.@HOST@"),
+ Node = get_node({Name, RoomHost}),
+ gen_server:call({Proc, Node}, {create, Name, From, Nick, Opts}).
store_room(ServerHost, Host, Name, Opts) ->
LServer = jlib:nameprep(ServerHost),
@@ -225,6 +274,46 @@ can_use_nick(LServer, Host, JID, Nick, odbc) ->
true
end.
+migrate(_Node, _UpOrDown, After) ->
+ Rs = mnesia:dirty_select(
+ muc_online_room,
+ [{#muc_online_room{name_host = '$1', pid = '$2', _ = '_'},
+ [],
+ ['$$']}]),
+ lists:foreach(
+ fun([NameHost, Pid]) ->
+ case get_node(NameHost) of
+ Node when Node /= node() ->
+ mod_muc_room:migrate(Pid, Node, random:uniform(After));
+ _ ->
+ ok
+ end
+ end, Rs).
+
+node_up(_Node) ->
+ copy_rooms(mnesia:dirty_first(muc_online_room)).
+
+node_down(Node) when Node == node() ->
+ copy_rooms(mnesia:dirty_first(muc_online_room));
+node_down(_) ->
+ ok.
+
+copy_rooms('$end_of_table') ->
+ ok;
+copy_rooms(Key) ->
+ case mnesia:dirty_read(muc_online_room, Key) of
+ [#muc_online_room{name_host = NameHost} = Room] ->
+ case get_node_new(NameHost) of
+ Node when node() /= Node ->
+ rpc:cast(Node, mnesia, dirty_write, [Room]);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end,
+ copy_rooms(mnesia:dirty_next(muc_online_room, Key)).
+
%%====================================================================
%% gen_server callbacks
%%====================================================================
@@ -240,6 +329,7 @@ init([Host, Opts]) ->
MyHost = gen_mod:get_opt_host(Host, Opts, "conference.@HOST@"),
case gen_mod:db_type(Opts) of
mnesia ->
+ update_muc_online_table(),
update_tables(MyHost),
mnesia:create_table(muc_room,
[{disc_copies, [node()]},
@@ -255,28 +345,34 @@ init([Host, Opts]) ->
end,
mnesia:create_table(muc_online_room,
[{ram_copies, [node()]},
+ {local_content, true},
{attributes, record_info(fields, muc_online_room)}]),
mnesia:add_table_copy(muc_online_room, node(), ram_copies),
catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]),
- clean_table_from_bad_node(node(), MyHost),
mnesia:subscribe(system),
Access = gen_mod:get_opt(access, Opts, all),
AccessCreate = gen_mod:get_opt(access_create, Opts, all),
AccessAdmin = gen_mod:get_opt(access_admin, Opts, none),
AccessPersistent = gen_mod:get_opt(access_persistent, Opts, all),
HistorySize = gen_mod:get_opt(history_size, Opts, 20),
+ PersistHistory = gen_mod:get_opt(persist_history, Opts, false),
DefRoomOpts = gen_mod:get_opt(default_room_options, Opts, []),
RoomShaper = gen_mod:get_opt(room_shaper, Opts, none),
ejabberd_router:register_route(MyHost),
+ ejabberd_hooks:add(node_up, ?MODULE, node_up, 100),
+ ejabberd_hooks:add(node_down, ?MODULE, node_down, 100),
+ ejabberd_hooks:add(node_hash_update, ?MODULE, migrate, 100),
load_permanent_rooms(MyHost, Host,
{Access, AccessCreate, AccessAdmin, AccessPersistent},
HistorySize,
+ PersistHistory,
RoomShaper),
{ok, #state{host = MyHost,
server_host = Host,
access = {Access, AccessCreate, AccessAdmin, AccessPersistent},
default_room_opts = DefRoomOpts,
history_size = HistorySize,
+ persist_history = PersistHistory,
room_shaper = RoomShaper}}.
%%--------------------------------------------------------------------
@@ -298,6 +394,7 @@ handle_call({create, Room, From, Nick, Opts},
access = Access,
default_room_opts = DefOpts,
history_size = HistorySize,
+ persist_history = PersistHistory,
room_shaper = RoomShaper} = State) ->
?DEBUG("MUC: create new room '~s'~n", [Room]),
NewOpts = case Opts of
@@ -306,7 +403,7 @@ handle_call({create, Room, From, Nick, Opts},
end,
{ok, Pid} = mod_muc_room:start(
Host, ServerHost, Access,
- Room, HistorySize,
+ Room, HistorySize, PersistHistory,
RoomShaper, From,
Nick, NewOpts),
register_room(Host, Room, Pid),
@@ -333,13 +430,21 @@ handle_info({route, From, To, Packet},
access = Access,
default_room_opts = DefRoomOpts,
history_size = HistorySize,
+ persist_history = PersistHistory,
room_shaper = RoomShaper} = State) ->
- case catch do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
- From, To, Packet, DefRoomOpts) of
- {'EXIT', Reason} ->
- ?ERROR_MSG("~p", [Reason]);
- _ ->
- ok
+ {U, S, _} = jlib:jid_tolower(To),
+ case get_node({U, S}) of
+ Node when Node == node() ->
+ case catch do_route(Host, ServerHost, Access, HistorySize, PersistHistory,
+ RoomShaper, From, To, Packet, DefRoomOpts) of
+ {'EXIT', Reason} ->
+ ?ERROR_MSG("~p", [Reason]);
+ _ ->
+ ok
+ end;
+ Node ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?PROCNAME),
+ {Proc, Node} ! {route, From, To, Packet}
end,
{noreply, State};
handle_info({room_destroyed, RoomHost, Pid}, State) ->
@@ -347,10 +452,15 @@ handle_info({room_destroyed, RoomHost, Pid}, State) ->
mnesia:delete_object(#muc_online_room{name_host = RoomHost,
pid = Pid})
end,
- mnesia:transaction(F),
- {noreply, State};
-handle_info({mnesia_system_event, {mnesia_down, Node}}, State) ->
- clean_table_from_bad_node(Node),
+ mnesia:async_dirty(F),
+ case get_node_new(RoomHost) of
+ Node when Node /= node() ->
+ rpc:cast(Node, mnesia, dirty_delete_object,
+ [#muc_online_room{name_host = RoomHost,
+ pid = Pid}]);
+ _ ->
+ ok
+ end,
{noreply, State};
handle_info(_Info, State) ->
{noreply, State}.
@@ -363,6 +473,9 @@ handle_info(_Info, State) ->
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, State) ->
+ ejabberd_hooks:delete(node_up, ?MODULE, node_up, 100),
+ ejabberd_hooks:delete(node_down, ?MODULE, node_down, 100),
+ ejabberd_hooks:delete(node_hash_update, ?MODULE, migrate, 100),
ejabberd_router:unregister_route(State#state.host),
ok.
@@ -393,12 +506,12 @@ stop_supervisor(Host) ->
supervisor:terminate_child(ejabberd_sup, Proc),
supervisor:delete_child(ejabberd_sup, Proc).
-do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
+do_route(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper,
From, To, Packet, DefRoomOpts) ->
{AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent} = Access,
case acl:match_rule(ServerHost, AccessRoute, From) of
allow ->
- do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
+ do_route1(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper,
From, To, Packet, DefRoomOpts);
_ ->
{xmlelement, _Name, Attrs, _Els} = Packet,
@@ -410,7 +523,7 @@ do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
end.
-do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
+do_route1(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper,
From, To, Packet, DefRoomOpts) ->
{_AccessRoute, AccessCreate, AccessAdmin, _AccessPersistent} = Access,
{Room, _, Nick} = jlib:jid_tolower(To),
@@ -551,14 +664,20 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
AccessCreate, From,
Room) of
true ->
- {ok, Pid} = start_new_room(
- Host, ServerHost, Access,
- Room, HistorySize,
- RoomShaper, From,
- Nick, DefRoomOpts),
- register_room(Host, Room, Pid),
- mod_muc_room:route(Pid, From, Nick, Packet),
- ok;
+ case start_new_room(
+ Host, ServerHost, Access,
+ Room, HistorySize, PersistHistory,
+ RoomShaper, From,
+ Nick, DefRoomOpts) of
+ {ok, Pid} ->
+ mod_muc_room:route(Pid, From, Nick, Packet),
+ register_room(Host, Room, Pid),
+ ok;
+ _Err ->
+ Err = jlib:make_error_reply(
+ Packet, ?ERR_INTERNAL_SERVER_ERROR),
+ ejabberd_router:route(To, From, Err)
+ end;
false ->
Lang = xml:get_attr_s("xml:lang", Attrs),
ErrText = "Room creation is denied by service policy",
@@ -621,50 +740,91 @@ get_rooms(LServer, Host, odbc) ->
end, RoomOpts)
end.
-load_permanent_rooms(Host, ServerHost, Access, HistorySize, RoomShaper) ->
+load_permanent_rooms(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper) ->
lists:foreach(
fun(R) ->
{Room, Host} = R#muc_room.name_host,
- case mnesia:dirty_read(muc_online_room, {Room, Host}) of
- [] ->
- {ok, Pid} = mod_muc_room:start(
- Host,
- ServerHost,
- Access,
- Room,
- HistorySize,
- RoomShaper,
- R#muc_room.opts),
- register_room(Host, Room, Pid);
+ case get_node({Room, Host}) of
+ Node when Node == node() ->
+ case mnesia:dirty_read(muc_online_room, {Room, Host}) of
+ [] ->
+ case get_room_state_if_broadcasted(
+ {Room, Host}) of
+ {ok, RoomState} ->
+ mod_muc_room:start(
+ normal_state, RoomState);
+ error ->
+ {ok, Pid} = mod_muc_room:start(
+ Host,
+ ServerHost,
+ Access,
+ Room,
+ HistorySize,
+ PersistHistory,
+ RoomShaper,
+ R#muc_room.opts),
+ register_room(Host, Room, Pid);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end;
_ ->
ok
end
end, get_rooms(ServerHost, Host)).
start_new_room(Host, ServerHost, Access, Room,
- HistorySize, RoomShaper, From,
+ HistorySize, PersistHistory, RoomShaper, From,
Nick, DefRoomOpts) ->
- case restore_room(ServerHost, Room, Host) of
+ case get_room_state_if_broadcasted({Room, Host}) of
+ {ok, RoomState} ->
+ ?DEBUG("MUC: restore room '~s' from other node~n", [Room]),
+ mod_muc_room:start(normal_state, RoomState);
error ->
- ?DEBUG("MUC: open new room '~s'~n", [Room]),
- mod_muc_room:start(Host, ServerHost, Access,
- Room, HistorySize,
- RoomShaper, From,
- Nick, DefRoomOpts);
- Opts ->
- ?DEBUG("MUC: restore room '~s'~n", [Room]),
- mod_muc_room:start(Host, ServerHost, Access,
- Room, HistorySize,
- RoomShaper, Opts)
+ case restore_room(ServerHost, Room, Host) of
+ error ->
+ ?DEBUG("MUC: open new room '~s'~n", [Room]),
+ mod_muc_room:start(Host, ServerHost, Access,
+ Room, HistorySize, PersistHistory,
+ RoomShaper, From,
+ Nick, DefRoomOpts);
+ Opts ->
+ ?DEBUG("MUC: restore room '~s'~n", [Room]),
+ mod_muc_room:start(Host, ServerHost, Access,
+ Room, HistorySize, PersistHistory,
+ RoomShaper, Opts)
+ end
end.
register_room(Host, Room, Pid) ->
F = fun() ->
- mnesia:write(#muc_online_room{name_host = {Room, Host},
- pid = Pid})
- end,
- mnesia:transaction(F).
-
+ mnesia:write(#muc_online_room{name_host = {Room, Host},
+ pid = Pid})
+ end,
+ mnesia:async_dirty(F),
+ case get_node_new({Room, Host}) of
+ Node when Node /= node() ->
+ %% New node has just been added. But we may miss MUC records
+ %% copy procedure, so we copy the MUC record manually just
+ %% to make sure
+ rpc:cast(Node, mnesia, dirty_write,
+ [#muc_online_room{name_host = {Room, Host},
+ pid = Pid}]),
+ case get_node({Room, Host}) of
+ Node when node() /= Node ->
+ %% Migration to new node has completed, and seems like
+ %% we missed it, so we migrate the MUC room pid manually.
+ %% It is not a problem if we have already got migration
+ %% notification: dups are just ignored by the MUC room pid.
+ mod_muc_room:migrate(Pid, Node, 0);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end.
iq_disco_info(Lang) ->
[{xmlelement, "identity",
@@ -693,7 +853,7 @@ iq_disco_items(Host, From, Lang, none) ->
_ ->
false
end
- end, get_vh_rooms(Host));
+ end, get_vh_rooms_all_nodes(Host));
iq_disco_items(Host, From, Lang, Rsm) ->
{Rooms, RsmO} = get_vh_rooms(Host, Rsm),
@@ -713,19 +873,9 @@ iq_disco_items(Host, From, Lang, Rsm) ->
end, Rooms) ++ RsmOut.
get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})->
- AllRooms = lists:sort(get_vh_rooms(Host)),
+ AllRooms = get_vh_rooms_all_nodes(Host),
Count = erlang:length(AllRooms),
- Guard = case Direction of
- _ when Index =/= undefined -> [{'==', {element, 2, '$1'}, Host}];
- aft -> [{'==', {element, 2, '$1'}, Host}, {'>=',{element, 1, '$1'} ,I}];
- before when I =/= []-> [{'==', {element, 2, '$1'}, Host}, {'=<',{element, 1, '$1'} ,I}];
- _ -> [{'==', {element, 2, '$1'}, Host}]
- end,
- L = lists:sort(
- mnesia:dirty_select(muc_online_room,
- [{#muc_online_room{name_host = '$1', _ = '_'},
- Guard,
- ['$_']}])),
+ L = get_vh_rooms_direction(Direction, I, Index, AllRooms),
L2 = if
Index == undefined andalso Direction == before ->
lists:reverse(lists:sublist(lists:reverse(L), 1, M));
@@ -748,6 +898,27 @@ get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})->
{L2, #rsm_out{first=F, last=Last, count=Count, index=NewIndex}}
end.
+get_vh_rooms_direction(_Direction, _I, Index, AllRooms) when Index =/= undefined ->
+ AllRooms;
+get_vh_rooms_direction(aft, I, _Index, AllRooms) ->
+ {_Before, After} =
+ lists:splitwith(
+ fun(#muc_online_room{name_host = {Na, _}}) ->
+ Na < I end, AllRooms),
+ case After of
+ [] -> [];
+ [#muc_online_room{name_host = {I, _Host}} | AfterTail] -> AfterTail;
+ _ -> After
+ end;
+get_vh_rooms_direction(before, I, _Index, AllRooms) when I =/= []->
+ {Before, _} =
+ lists:splitwith(
+ fun(#muc_online_room{name_host = {Na, _}}) ->
+ Na < I end, AllRooms),
+ Before;
+get_vh_rooms_direction(_Direction, _I, _Index, AllRooms) ->
+ AllRooms.
+
%% @doc Return the position of desired room in the list of rooms.
%% The room must exist in the list. The count starts in 0.
%% @spec (Desired::muc_online_room(), Rooms::[muc_online_room()]) -> integer()
@@ -977,7 +1148,22 @@ broadcast_service_message(Host, Msg) ->
fun(#muc_online_room{pid = Pid}) ->
gen_fsm:send_all_state_event(
Pid, {service_message, Msg})
- end, get_vh_rooms(Host)).
+ end, get_vh_rooms_all_nodes(Host)).
+
+get_vh_rooms_all_nodes(Host) ->
+ Rooms = lists:foldl(
+ fun(Node, Acc) when Node == node() ->
+ get_vh_rooms(Host) ++ Acc;
+ (Node, Acc) ->
+ case catch rpc:call(Node, ?MODULE, get_vh_rooms,
+ [Host], 5000) of
+ Res when is_list(Res) ->
+ Res ++ Acc;
+ _ ->
+ Acc
+ end
+ end, [], get_nodes(Host)),
+ lists:ukeysort(#muc_online_room.name_host, Rooms).
get_vh_rooms(Host) ->
mnesia:dirty_select(muc_online_room,
@@ -985,39 +1171,18 @@ get_vh_rooms(Host) ->
[{'==', {element, 2, '$1'}, Host}],
['$_']}]).
-
-clean_table_from_bad_node(Node) ->
- F = fun() ->
- Es = mnesia:select(
- muc_online_room,
- [{#muc_online_room{pid = '$1', _ = '_'},
- [{'==', {node, '$1'}, Node}],
- ['$_']}]),
- lists:foreach(fun(E) ->
- mnesia:delete_object(E)
- end, Es)
- end,
- mnesia:async_dirty(F).
-
-clean_table_from_bad_node(Node, Host) ->
- F = fun() ->
- Es = mnesia:select(
- muc_online_room,
- [{#muc_online_room{pid = '$1',
- name_host = {'_', Host},
- _ = '_'},
- [{'==', {node, '$1'}, Node}],
- ['$_']}]),
- lists:foreach(fun(E) ->
- mnesia:delete_object(E)
- end, Es)
- end,
- mnesia:async_dirty(F).
-
update_tables(Host) ->
update_muc_room_table(Host),
update_muc_registered_table(Host).
+update_muc_online_table() ->
+ case catch mnesia:table_info(muc_online_room, local_content) of
+ false ->
+ mnesia:delete_table(muc_online_room);
+ _ ->
+ ok
+ end.
+
update_muc_room_table(Host) ->
Fields = record_info(fields, muc_room),
case mnesia:table_info(muc_room, attributes) of
@@ -1101,3 +1266,67 @@ update_muc_registered_table(Host) ->
?INFO_MSG("Recreating muc_registered table", []),
mnesia:transform_table(muc_registered, ignore, Fields)
end.
+
+is_broadcasted(RoomHost) ->
+ case ejabberd_config:get_local_option({domain_balancing, RoomHost}) of
+ broadcast ->
+ true;
+ _ ->
+ false
+ end.
+
+get_node({_, RoomHost} = Key) ->
+ case is_broadcasted(RoomHost) of
+ true ->
+ node();
+ false ->
+ ejabberd_cluster:get_node(Key)
+ end;
+get_node(RoomHost) ->
+ get_node({"", RoomHost}).
+
+get_node_new({_, RoomHost} = Key) ->
+ case is_broadcasted(RoomHost) of
+ true ->
+ node();
+ false ->
+ ejabberd_cluster:get_node_new(Key)
+ end;
+get_node_new(RoomHost) ->
+ get_node_new({"", RoomHost}).
+
+get_nodes(RoomHost) ->
+ case is_broadcasted(RoomHost) of
+ true ->
+ [node()];
+ false ->
+ ejabberd_cluster:get_nodes()
+ end.
+
+get_room_state_if_broadcasted({Room, Host}) ->
+ case is_broadcasted(Host) of
+ true ->
+ lists:foldl(
+ fun(_, {ok, StateData}) ->
+ {ok, StateData};
+ (Node, _) when Node /= node() ->
+ case catch rpc:call(
+ Node, mnesia, dirty_read,
+ [muc_online_room, {Room, Host}], 5000) of
+ [#muc_online_room{pid = Pid}] ->
+ case catch gen_fsm:sync_send_all_state_event(
+ Pid, get_state, 5000) of
+ {ok, StateData} ->
+ {ok, StateData};
+ _ ->
+ error
+ end;
+ _ ->
+ error
+ end;
+ (_, Acc) ->
+ Acc
+ end, error, ejabberd_cluster:get_nodes());
+ false ->
+ error
+ end.
diff --git a/src/mod_muc/mod_muc_log.erl b/src/mod_muc/mod_muc_log.erl
index 04b91bf8c..47d4a3c96 100644
--- a/src/mod_muc/mod_muc_log.erl
+++ b/src/mod_muc/mod_muc_log.erl
@@ -73,11 +73,11 @@
%% Description: Starts the server
%%--------------------------------------------------------------------
start_link(Host, Opts) ->
- Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
- gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+ Proc = get_proc_name(Host),
+ gen_server:start_link(Proc, ?MODULE, [Host, Opts], []).
start(Host, Opts) ->
- Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ Proc = get_proc_name(Host),
ChildSpec =
{Proc,
{?MODULE, start_link, [Host, Opts]},
@@ -88,7 +88,7 @@ start(Host, Opts) ->
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
- Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ Proc = get_proc_name(Host),
gen_server:call(Proc, stop),
supervisor:delete_child(ejabberd_sup, Proc).
@@ -951,7 +951,8 @@ get_room_state(RoomPid) ->
{ok, R} = gen_fsm:sync_send_all_state_event(RoomPid, get_state),
R.
-get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?PROCNAME).
+get_proc_name(Host) ->
+ {global, gen_mod:get_module_proc(Host, ?PROCNAME)}.
calc_hour_offset(TimeHere) ->
TimeZero = calendar:now_to_universal_time(now()),
diff --git a/src/mod_muc/mod_muc_room.erl b/src/mod_muc/mod_muc_room.erl
index 670460be7..ecf9c9581 100644
--- a/src/mod_muc/mod_muc_room.erl
+++ b/src/mod_muc/mod_muc_room.erl
@@ -27,15 +27,22 @@
-module(mod_muc_room).
-author('alexey@process-one.net').
--behaviour(gen_fsm).
+-define(GEN_FSM, p1_fsm).
+
+-behaviour(?GEN_FSM).
%% External exports
--export([start_link/9,
- start_link/7,
- start/9,
- start/7,
- route/4]).
+-export([start_link/10,
+ start_link/8,
+ start_link/2,
+ start/10,
+ start/8,
+ start/2,
+ migrate/3,
+ route/4,
+ moderate_room_history/2,
+ persist_recent_messages/1]).
%% gen_fsm callbacks
-export([init/1,
@@ -44,6 +51,7 @@
handle_sync_event/4,
handle_info/3,
terminate/3,
+ print_state/1,
code_change/4]).
-include("ejabberd.hrl").
@@ -63,42 +71,54 @@
%% Module start with or without supervisor:
-ifdef(NO_TRANSIENT_SUPERVISORS).
--define(SUPERVISOR_START,
- gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize,
- RoomShaper, Creator, Nick, DefRoomOpts],
- ?FSMOPTS)).
+-define(SUPERVISOR_START(Args),
+ ?GEN_FSM:start(?MODULE, Args, ?FSMOPTS)).
-else.
--define(SUPERVISOR_START,
+-define(SUPERVISOR_START(Args),
Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup),
- supervisor:start_child(
- Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper,
- Creator, Nick, DefRoomOpts])).
+ supervisor:start_child(Supervisor, Args)).
-endif.
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
-start(Host, ServerHost, Access, Room, HistorySize, RoomShaper,
+start(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper,
Creator, Nick, DefRoomOpts) ->
- ?SUPERVISOR_START.
+ ?SUPERVISOR_START([Host, ServerHost, Access, Room, HistorySize, PersistHistory,
+ RoomShaper, Creator, Nick, DefRoomOpts]).
-start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) ->
+start(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Opts) ->
Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup),
supervisor:start_child(
- Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper,
+ Supervisor, [Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper,
Opts]).
-start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper,
+start(StateName, StateData) ->
+ ServerHost = StateData#state.server_host,
+ ?SUPERVISOR_START([StateName, StateData]).
+
+start_link(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper,
Creator, Nick, DefRoomOpts) ->
- gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize,
- RoomShaper, Creator, Nick, DefRoomOpts],
- ?FSMOPTS).
+ ?GEN_FSM:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, PersistHistory,
+ RoomShaper, Creator, Nick, DefRoomOpts],
+ ?FSMOPTS).
+
+start_link(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Opts) ->
+ ?GEN_FSM:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, PersistHistory,
+ RoomShaper, Opts],
+ ?FSMOPTS).
+
+start_link(StateName, StateData) ->
+ ?GEN_FSM:start_link(?MODULE, [StateName, StateData], ?FSMOPTS).
-start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) ->
- gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize,
- RoomShaper, Opts],
- ?FSMOPTS).
+migrate(FsmRef, Node, After) ->
+ erlang:send_after(After, FsmRef, {migrate, Node}).
+moderate_room_history(FsmRef, Nick) ->
+ ?GEN_FSM:sync_send_all_state_event(FsmRef, {moderate_room_history, Nick}).
+
+persist_recent_messages(FsmRef) ->
+ ?GEN_FSM:sync_send_all_state_event(FsmRef, persist_recent_messages).
%%%----------------------------------------------------------------------
%%% Callback functions from gen_fsm
%%%----------------------------------------------------------------------
@@ -110,7 +130,7 @@ start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) ->
%% ignore |
%% {stop, StopReason}
%%----------------------------------------------------------------------
-init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, DefRoomOpts]) ->
+init([Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Creator, _Nick, DefRoomOpts]) ->
process_flag(trap_exit, true),
Shaper = shaper:new(RoomShaper),
State = set_affiliation(Creator, owner,
@@ -119,27 +139,43 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, D
access = Access,
room = Room,
history = lqueue_new(HistorySize),
+ persist_history = PersistHistory,
jid = jlib:make_jid(Room, Host, ""),
just_created = true,
room_shaper = Shaper}),
State1 = set_opts(DefRoomOpts, State),
+ %% this will trigger a write of the muc to disc if it is persistent.
+ %% we need to do this because otherwise if muc are persistent by default,
+ %% but never configured in any way by the client, we were never
+ %% storing it on disc to be recreated on startup.
+ if
+ (State1#state.config)#config.persistent ->
+ mod_muc:store_room(State1#state.host, State1#state.room, make_opts(State1));
+ true ->
+ ok
+ end,
?INFO_MSG("Created MUC room ~s@~s by ~s",
[Room, Host, jlib:jid_to_string(Creator)]),
add_to_log(room_existence, created, State1),
add_to_log(room_existence, started, State1),
{ok, normal_state, State1};
-init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) ->
+init([Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Opts]) ->
process_flag(trap_exit, true),
Shaper = shaper:new(RoomShaper),
State = set_opts(Opts, #state{host = Host,
server_host = ServerHost,
access = Access,
room = Room,
- history = lqueue_new(HistorySize),
+ history = load_history(ServerHost, Room, PersistHistory, lqueue_new(HistorySize)),
+ persist_history = PersistHistory,
jid = jlib:make_jid(Room, Host, ""),
room_shaper = Shaper}),
add_to_log(room_existence, started, State),
- {ok, normal_state, State}.
+ {ok, normal_state, State};
+init([StateName, #state{room = Room, host = Host} = StateData]) ->
+ process_flag(trap_exit, true),
+ mod_muc:register_room(Host, Room, self()),
+ {ok, StateName, StateData}.
%%----------------------------------------------------------------------
%% Func: StateName/2
@@ -170,7 +206,7 @@ normal_state({route, From, "",
ErrText = "Traffic rate limit is exceeded",
Err = jlib:make_error_reply(
Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
From, Err),
{next_state, normal_state, StateData};
@@ -251,7 +287,7 @@ normal_state({route, From, "",
ErrText = "It is not allowed to send private messages to the conference",
Err = jlib:make_error_reply(
Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
From, Err),
{next_state, normal_state, StateData};
@@ -266,7 +302,7 @@ normal_state({route, From, "",
{error, Error} ->
Err = jlib:make_error_reply(
Packet, Error),
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
From, Err),
{next_state, normal_state, StateData};
@@ -332,7 +368,7 @@ normal_state({route, From, "",
Packet,
?ERRT_NOT_ACCEPTABLE(
Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
From, Err),
StateData#state{
@@ -346,7 +382,7 @@ normal_state({route, From, "",
Packet,
?ERRT_FORBIDDEN(
Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid, From, Err),
StateData
end,
@@ -364,7 +400,7 @@ normal_state({route, From, "",
Packet,
?ERRT_BAD_REQUEST(
Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
From, Err),
StateData;
@@ -392,7 +428,7 @@ normal_state({route, From, "",
Packet,
?ERRT_NOT_ALLOWED(
Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid, From, Err),
StateData
end,
@@ -404,7 +440,7 @@ normal_state({route, From, "",
ErrText = "Improper message type",
Err = jlib:make_error_reply(
Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
From, Err),
{next_state, normal_state, StateData}
@@ -455,7 +491,7 @@ normal_state({route, From, "",
sub_el = [SubEl, Error]},
StateData}
end,
- ejabberd_router:route(StateData#state.jid,
+ route_stanza(StateData#state.jid,
From,
jlib:iq_to_xml(IQRes)),
case NewStateData of
@@ -469,7 +505,7 @@ normal_state({route, From, "",
_ ->
Err = jlib:make_error_reply(
Packet, ?ERR_FEATURE_NOT_IMPLEMENTED),
- ejabberd_router:route(StateData#state.jid, From, Err),
+ route_stanza(StateData#state.jid, From, Err),
{next_state, normal_state, StateData}
end;
@@ -528,7 +564,7 @@ normal_state({route, From, ToNick,
"messages of type \"groupchat\"",
Err = jlib:make_error_reply(
Packet, ?ERRT_BAD_REQUEST(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(
StateData#state.jid,
ToNick),
@@ -539,7 +575,7 @@ normal_state({route, From, ToNick,
ErrText = "Recipient is not in the conference room",
Err = jlib:make_error_reply(
Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(
StateData#state.jid,
ToNick),
@@ -555,12 +591,12 @@ normal_state({route, From, ToNick,
?DICT:find(jlib:jid_tolower(From),
StateData#state.users),
FromNickJID = jlib:jid_replace_resource(StateData#state.jid, FromNick),
- [ejabberd_router:route(FromNickJID, ToJID, Packet) || ToJID <- ToJIDs];
+ [route_stanza(FromNickJID, ToJID, Packet) || ToJID <- ToJIDs];
true ->
ErrText = "It is not allowed to send private messages",
Err = jlib:make_error_reply(
Packet, ?ERRT_FORBIDDEN(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(
StateData#state.jid,
ToNick),
@@ -572,7 +608,7 @@ normal_state({route, From, ToNick,
ErrText = "Only occupants are allowed to send messages to the conference",
Err = jlib:make_error_reply(
Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(
StateData#state.jid,
ToNick),
@@ -581,7 +617,7 @@ normal_state({route, From, ToNick,
ErrText = "It is not allowed to send private messages",
Err = jlib:make_error_reply(
Packet, ?ERRT_FORBIDDEN(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(
StateData#state.jid,
ToNick),
@@ -607,7 +643,7 @@ normal_state({route, From, ToNick,
ErrText = "Recipient is not in the conference room",
Err = jlib:make_error_reply(
Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(
StateData#state.jid, ToNick),
From, Err)
@@ -618,7 +654,7 @@ normal_state({route, From, ToNick,
StateData#state.users),
{ToJID2, Packet2} = handle_iq_vcard(FromFull, ToJID,
StanzaId, NewId,Packet),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(StateData#state.jid, FromNick),
ToJID2, Packet2)
end;
@@ -630,7 +666,7 @@ normal_state({route, From, ToNick,
ErrText = "Only occupants are allowed to send queries to the conference",
Err = jlib:make_error_reply(
Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(StateData#state.jid, ToNick),
From, Err)
end;
@@ -642,7 +678,7 @@ normal_state({route, From, ToNick,
ErrText = "Queries to the conference members are not allowed in this room",
Err = jlib:make_error_reply(
Packet, ?ERRT_NOT_ALLOWED(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(StateData#state.jid, ToNick),
From, Err)
end
@@ -666,7 +702,7 @@ handle_event({service_message, Msg}, _StateName, StateData) ->
[{xmlelement, "body", [], [{xmlcdata, Msg}]}]},
lists:foreach(
fun({_LJID, Info}) ->
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
Info#user.jid,
MessagePkt)
@@ -713,6 +749,16 @@ handle_event(_Event, StateName, StateData) ->
%% {stop, Reason, NewStateData} |
%% {stop, Reason, Reply, NewStateData}
%%----------------------------------------------------------------------
+handle_sync_event({moderate_room_history, Nick}, _From, StateName, #state{history = History} = StateData) ->
+ NewHistory = lqueue_filter(fun({FromNick, _TSPacket, _HaveSubject, _Timestamp, _Size}) ->
+ FromNick /= Nick
+ end, History),
+ Moderated = History#lqueue.len - NewHistory#lqueue.len,
+ {reply, {ok, integer_to_list(Moderated)}, StateName, StateData#state{history = NewHistory}};
+
+handle_sync_event(persist_recent_messages, _From, StateName, StateData) ->
+ {reply, persist_muc_history(StateData), StateName, StateData};
+
handle_sync_event({get_disco_item, JID, Lang}, _From, StateName, StateData) ->
Reply = get_roomdesc_reply(JID, StateData,
get_roomdesc_tail(StateData, Lang)),
@@ -733,6 +779,9 @@ handle_sync_event(_Event, _From, StateName, StateData) ->
code_change(_OldVsn, StateName, StateData, _Extra) ->
{ok, StateName, StateData}.
+print_state(StateData) ->
+ StateData.
+
%%----------------------------------------------------------------------
%% Func: handle_info/3
%% Returns: {next_state, NextStateName, NextStateData} |
@@ -805,7 +854,7 @@ handle_info({captcha_failed, From}, normal_state, StateData) ->
Robots = ?DICT:erase(From, StateData#state.robots),
Err = jlib:make_error_reply(
Packet, ?ERR_NOT_AUTHORIZED),
- ejabberd_router:route( % TODO: s/Nick/""/
+ route_stanza( % TODO: s/Nick/""/
jlib:jid_replace_resource(
StateData#state.jid, Nick),
From, Err),
@@ -814,6 +863,15 @@ handle_info({captcha_failed, From}, normal_state, StateData) ->
StateData
end,
{next_state, normal_state, NewState};
+handle_info({migrate, Node}, StateName, StateData) ->
+ if Node /= node() ->
+ {migrate, StateData,
+ {Node, ?MODULE, start, [StateName, StateData]}, 0};
+ true ->
+ {next_state, StateName, StateData}
+ end;
+handle_info('shutdown', _StateName, StateData) ->
+ {stop, 'shutdown', StateData};
handle_info(_Info, StateName, StateData) ->
{next_state, StateName, StateData}.
@@ -822,6 +880,13 @@ handle_info(_Info, StateName, StateData) ->
%% Purpose: Shutdown the fsm
%% Returns: any
%%----------------------------------------------------------------------
+terminate({migrated, Clone}, _StateName, StateData) ->
+ ?INFO_MSG("Migrating room ~s@~s to ~p on node ~p",
+ [StateData#state.room, StateData#state.host,
+ Clone, node(Clone)]),
+ mod_muc:room_destroyed(StateData#state.host, StateData#state.room,
+ self(), StateData#state.server_host),
+ ok;
terminate(Reason, _StateName, StateData) ->
?INFO_MSG("Stopping MUC room ~s@~s",
[StateData#state.room, StateData#state.host]),
@@ -842,7 +907,7 @@ terminate(Reason, _StateName, StateData) ->
Nick = Info#user.nick,
case Reason of
shutdown ->
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(StateData#state.jid, Nick),
Info#user.jid,
Packet);
@@ -851,6 +916,12 @@ terminate(Reason, _StateName, StateData) ->
tab_remove_online_user(LJID, StateData)
end, [], StateData#state.users),
add_to_log(room_existence, stopped, StateData),
+ if
+ Reason == 'shutdown' ->
+ persist_muc_history(StateData);
+ true ->
+ ok
+ end,
mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(),
StateData#state.server_host),
ok.
@@ -859,8 +930,48 @@ terminate(Reason, _StateName, StateData) ->
%%% Internal functions
%%%----------------------------------------------------------------------
+load_history(_Host, _Room, false, Queue) ->
+ Queue;
+load_history(Host, Room, true, Queue) ->
+ ?INFO_MSG("Loading history for room ~s on host ~s", [Room, Host]),
+ case odbc_queries:load_roomhistory(Host, ejabberd_odbc:escape(Room)) of
+ {selected, ["nick", "packet", "have_subject", "timestamp", "size"], Items} ->
+ ?DEBUG("Found ~p messages on history for ~s", [length(Items), Room]),
+ lists:foldl(fun(I, Q) ->
+ {Nick, XML, HS, Ts, Size} = I,
+ Item = {Nick,
+ xml_stream:parse_element(XML),
+ HS /= "0",
+ calendar:gregorian_seconds_to_datetime(list_to_integer(Ts)),
+ list_to_integer(Size)},
+ lqueue_in(Item, Q)
+ end, Queue, Items);
+ _ ->
+ Queue
+ end.
+
+
+persist_muc_history(#state{room = Room, server_host = Server, config = #config{persistent = true} ,persist_history = true, history = Q}) ->
+ ?INFO_MSG("Persisting history for room ~s on host ~s", [Room, Server]),
+ Queries = lists:map(fun({FromNick, Packet, HaveSubject, Timestamp, Size}) ->
+ odbc_queries:add_roomhistory_sql(
+ ejabberd_odbc:escape(Room),
+ ejabberd_odbc:escape(FromNick),
+ ejabberd_odbc:escape(xml:element_to_binary(Packet)),
+ atom_to_list(HaveSubject),
+ integer_to_list(calendar:datetime_to_gregorian_seconds(Timestamp)),
+ integer_to_list(Size))
+ end, lqueue_to_list(Q)),
+ odbc_queries:clear_and_add_roomhistory(Server,ejabberd_odbc:escape(Room), Queries),
+ {ok, {persisted, length(Queries)}};
+ %% en mod_muc, cuando se levantan los muc persistentes, si se crea, y el flag persist_history esta en true,
+ %% se levantan los mensajes persistentes tb.
+
+persist_muc_history(_) ->
+ {ok, not_persistent}.
+
route(Pid, From, ToNick, Packet) ->
- gen_fsm:send_event(Pid, {route, From, ToNick, Packet}).
+ ?GEN_FSM:send_event(Pid, {route, From, ToNick, Packet}).
process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet,
StateData) ->
@@ -904,7 +1015,7 @@ process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet,
true ->
lists:foreach(
fun({_LJID, Info}) ->
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(
StateData#state.jid,
FromNick),
@@ -932,7 +1043,7 @@ process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet,
"Only moderators "
"are allowed to change the subject in this room")
end,
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
From,
jlib:make_error_reply(Packet, Err)),
@@ -942,7 +1053,7 @@ process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet,
ErrText = "Visitors are not allowed to send messages to all occupants",
Err = jlib:make_error_reply(
Packet, ?ERRT_FORBIDDEN(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
From, Err),
{next_state, normal_state, StateData}
@@ -951,7 +1062,7 @@ process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet,
ErrText = "Only occupants are allowed to send messages to the conference",
Err = jlib:make_error_reply(
Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
- ejabberd_router:route(StateData#state.jid, From, Err),
+ route_stanza(StateData#state.jid, From, Err),
{next_state, normal_state, StateData}
end.
@@ -1036,7 +1147,7 @@ process_presence(From, Nick, {xmlelement, "presence", Attrs, _Els} = Packet,
Err = jlib:make_error_reply(
Packet,
?ERRT_NOT_ALLOWED(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
% TODO: s/Nick/""/
jlib:jid_replace_resource(
StateData#state.jid,
@@ -1049,7 +1160,7 @@ process_presence(From, Nick, {xmlelement, "presence", Attrs, _Els} = Packet,
Err = jlib:make_error_reply(
Packet,
?ERRT_CONFLICT(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(
StateData#state.jid,
Nick), % TODO: s/Nick/""/
@@ -1060,7 +1171,7 @@ process_presence(From, Nick, {xmlelement, "presence", Attrs, _Els} = Packet,
Err = jlib:make_error_reply(
Packet,
?ERRT_CONFLICT(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
% TODO: s/Nick/""/
jlib:jid_replace_resource(
StateData#state.jid,
@@ -1752,7 +1863,7 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
Err = jlib:make_error_reply(
Packet,
?ERR_SERVICE_UNAVAILABLE),
- ejabberd_router:route( % TODO: s/Nick/""/
+ route_stanza( % TODO: s/Nick/""/
jlib:jid_replace_resource(StateData#state.jid, Nick),
From, Err),
StateData;
@@ -1767,14 +1878,14 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
ErrText = "Membership is required to enter this room",
?ERRT_REGISTRATION_REQUIRED(Lang, ErrText)
end),
- ejabberd_router:route( % TODO: s/Nick/""/
+ route_stanza( % TODO: s/Nick/""/
jlib:jid_replace_resource(StateData#state.jid, Nick),
From, Err),
StateData;
{_, true, _, _} ->
ErrText = "That nickname is already in use by another occupant",
Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
% TODO: s/Nick/""/
jlib:jid_replace_resource(StateData#state.jid, Nick),
From, Err),
@@ -1782,7 +1893,7 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
{_, _, false, _} ->
ErrText = "That nickname is registered by another person",
Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)),
- ejabberd_router:route(
+ route_stanza(
% TODO: s/Nick/""/
jlib:jid_replace_resource(StateData#state.jid, Nick),
From, Err),
@@ -1803,7 +1914,7 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
"This room is not anonymous")}]},
{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
[{xmlelement, "status", [{"code", "100"}], []}]}]},
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
From, WPacket);
true ->
@@ -1829,7 +1940,7 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
ErrText = "A password is required to enter this room",
Err = jlib:make_error_reply(
Packet, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)),
- ejabberd_router:route( % TODO: s/Nick/""/
+ route_stanza( % TODO: s/Nick/""/
jlib:jid_replace_resource(
StateData#state.jid, Nick),
From, Err),
@@ -1845,13 +1956,13 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
MsgPkt = {xmlelement, "message", [{"id", ID}], CaptchaEls},
Robots = ?DICT:store(From,
{Nick, Packet}, StateData#state.robots),
- ejabberd_router:route(RoomJID, From, MsgPkt),
+ route_stanza(RoomJID, From, MsgPkt),
StateData#state{robots = Robots};
{error, limit} ->
ErrText = "Too many CAPTCHA requests",
Err = jlib:make_error_reply(
Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)),
- ejabberd_router:route( % TODO: s/Nick/""/
+ route_stanza( % TODO: s/Nick/""/
jlib:jid_replace_resource(
StateData#state.jid, Nick),
From, Err),
@@ -1860,7 +1971,7 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
ErrText = "Unable to generate a CAPTCHA",
Err = jlib:make_error_reply(
Packet, ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText)),
- ejabberd_router:route( % TODO: s/Nick/""/
+ route_stanza( % TODO: s/Nick/""/
jlib:jid_replace_resource(
StateData#state.jid, Nick),
From, Err),
@@ -1870,7 +1981,7 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
ErrText = "Incorrect password",
Err = jlib:make_error_reply(
Packet, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)),
- ejabberd_router:route( % TODO: s/Nick/""/
+ route_stanza( % TODO: s/Nick/""/
jlib:jid_replace_resource(
StateData#state.jid, Nick),
From, Err),
@@ -2146,7 +2257,7 @@ send_new_presence(NJID, Reason, StateData) ->
Presence,
[{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
[{xmlelement, "item", ItemAttrs, ItemEls} | Status3]}]),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(StateData#state.jid, Nick),
Info#user.jid,
Packet)
@@ -2188,7 +2299,7 @@ send_existing_presences(ToJID, StateData) ->
Presence,
[{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
[{xmlelement, "item", ItemAttrs, []}]}]),
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(
StateData#state.jid, FromNick),
RealToJID,
@@ -2286,7 +2397,7 @@ send_nick_changing(JID, OldNick, StateData,
[{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
[{xmlelement, "item", ItemAttrs2, []}]}]),
if SendOldUnavailable ->
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(StateData#state.jid, OldNick),
Info#user.jid,
Packet1);
@@ -2294,7 +2405,7 @@ send_nick_changing(JID, OldNick, StateData,
ok
end,
if SendNewAvailable ->
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(StateData#state.jid, Nick),
Info#user.jid,
Packet2);
@@ -2332,6 +2443,9 @@ lqueue_cut(Q, N) ->
lqueue_to_list(#lqueue{queue = Q1}) ->
queue:to_list(Q1).
+lqueue_filter(F, #lqueue{queue = Q1} = LQ) ->
+ Q2 = queue:filter(F, Q1),
+ LQ#lqueue{queue = Q2, len = queue:len(Q2)}.
add_message_to_history(FromNick, FromJID, Packet, StateData) ->
HaveSubject = case xml:get_subtag(Packet, "subject") of
@@ -2366,7 +2480,7 @@ add_message_to_history(FromNick, FromJID, Packet, StateData) ->
send_history(JID, Shift, StateData) ->
lists:foldl(
fun({Nick, Packet, HaveSubject, _TimeStamp, _Size}, B) ->
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(StateData#state.jid, Nick),
JID,
Packet),
@@ -2388,7 +2502,7 @@ send_subject(JID, Lang, StateData) ->
translate:translate(Lang,
" has set the subject to: ") ++
Subject}]}]},
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
JID,
Packet)
@@ -2972,7 +3086,7 @@ send_kickban_presence1(UJID, Reason, Code, Affiliation, StateData) ->
[{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
[{xmlelement, "item", ItemAttrs, ItemEls},
{xmlelement, "status", [{"code", Code}], []}]}]},
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(StateData#state.jid, Nick),
Info#user.jid,
Packet)
@@ -3612,7 +3726,7 @@ destroy_room(DEl, StateData) ->
Packet = {xmlelement, "presence", [{"type", "unavailable"}],
[{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
[{xmlelement, "item", ItemAttrs, []}, DEl]}]},
- ejabberd_router:route(
+ route_stanza(
jlib:jid_replace_resource(StateData#state.jid, Nick),
Info#user.jid,
Packet)
@@ -3811,7 +3925,7 @@ send_voice_request(From, StateData) ->
FromNick = find_nick_by_jid(From, StateData),
lists:foreach(
fun({_, User}) ->
- ejabberd_router:route(
+ route_stanza(
StateData#state.jid,
User#user.jid,
prepare_request_form(From, FromNick, ""))
@@ -3983,7 +4097,7 @@ check_invitation(From, Els, Lang, StateData) ->
""})}],
[{xmlcdata, Reason}]},
Body]},
- ejabberd_router:route(StateData#state.jid, JID, Msg),
+ route_stanza(StateData#state.jid, JID, Msg),
JID
end.
@@ -4021,7 +4135,7 @@ send_decline_invitation({Packet, XEl, DEl, ToJID}, RoomJID, FromJID) ->
DEl2 = {xmlelement, "decline", DAttrs3, DEls},
XEl2 = replace_subelement(XEl, DEl2),
Packet2 = replace_subelement(Packet, XEl2),
- ejabberd_router:route(RoomJID, ToJID, Packet2).
+ route_stanza(RoomJID, ToJID, Packet2).
%% Given an element and a new subelement,
%% replace the instance of the subelement in element with the new subelement.
@@ -4033,7 +4147,7 @@ replace_subelement({xmlelement, Name, Attrs, SubEls}, NewSubEl) ->
send_error_only_occupants(Packet, Lang, RoomJID, From) ->
ErrText = "Only occupants are allowed to send messages to the conference",
Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
- ejabberd_router:route(RoomJID, From, Err).
+ route_stanza(RoomJID, From, Err).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@@ -4091,3 +4205,17 @@ tab_count_user(JID) ->
element_size(El) ->
size(xml:element_to_binary(El)).
+
+route_stanza(From, To, El) ->
+ case mod_muc:is_broadcasted(From#jid.lserver) of
+ true ->
+ #jid{luser = LUser, lserver = LServer} = To,
+ case ejabberd_cluster:get_node({LUser, LServer}) of
+ Node when Node == node() ->
+ ejabberd_router:route(From, To, El);
+ _ ->
+ ok
+ end;
+ false ->
+ ejabberd_router:route(From, To, El)
+ end.
diff --git a/src/mod_muc/mod_muc_room.hrl b/src/mod_muc/mod_muc_room.hrl
index a7a6ca0b2..ecc044909 100644
--- a/src/mod_muc/mod_muc_room.hrl
+++ b/src/mod_muc/mod_muc_room.hrl
@@ -77,6 +77,7 @@
nicks = ?DICT:new(),
affiliations = ?DICT:new(),
history,
+ persist_history = false,
subject = "",
subject_author = "",
just_created = false,
diff --git a/src/mod_offline.erl b/src/mod_offline.erl
index 75b1966b2..68fbe4cd0 100644
--- a/src/mod_offline.erl
+++ b/src/mod_offline.erl
@@ -32,19 +32,18 @@
-export([count_offline_messages/2]).
-export([start/2,
- loop/2,
+ loop/2,
stop/1,
store_packet/3,
resend_offline_messages/2,
pop_offline_messages/3,
- get_sm_features/5,
remove_expired_messages/1,
remove_old_messages/2,
remove_user/2,
- get_queue_length/2,
webadmin_page/3,
webadmin_user/4,
- webadmin_user_parse_query/5]).
+ webadmin_user_parse_query/5,
+ count_offline_messages/3]).
-include("ejabberd.hrl").
-include("jlib.hrl").
@@ -79,19 +78,17 @@ start(Host, Opts) ->
?MODULE, remove_user, 50),
ejabberd_hooks:add(anonymous_purge_hook, Host,
?MODULE, remove_user, 50),
- ejabberd_hooks:add(disco_sm_features, Host,
- ?MODULE, get_sm_features, 50),
- ejabberd_hooks:add(disco_local_features, Host,
- ?MODULE, get_sm_features, 50),
ejabberd_hooks:add(webadmin_page_host, Host,
?MODULE, webadmin_page, 50),
ejabberd_hooks:add(webadmin_user, Host,
?MODULE, webadmin_user, 50),
ejabberd_hooks:add(webadmin_user_parse_query, Host,
?MODULE, webadmin_user_parse_query, 50),
+ ejabberd_hooks:add(count_offline_messages, Host,
+ ?MODULE, count_offline_messages, 50),
AccessMaxOfflineMsgs = gen_mod:get_opt(access_max_user_messages, Opts, max_user_offline_messages),
register(gen_mod:get_module_proc(Host, ?PROCNAME),
- spawn(?MODULE, loop, [Host, AccessMaxOfflineMsgs])).
+ spawn(?MODULE, loop, [Host, AccessMaxOfflineMsgs])).
loop(Host, AccessMaxOfflineMsgs) ->
receive
@@ -214,8 +211,6 @@ stop(Host) ->
?MODULE, remove_user, 50),
ejabberd_hooks:delete(anonymous_purge_hook, Host,
?MODULE, remove_user, 50),
- ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50),
- ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_sm_features, 50),
ejabberd_hooks:delete(webadmin_page_host, Host,
?MODULE, webadmin_page, 50),
ejabberd_hooks:delete(webadmin_user, Host,
@@ -226,27 +221,12 @@ stop(Host) ->
exit(whereis(Proc), stop),
{wait, Proc}.
-get_sm_features(Acc, _From, _To, "", _Lang) ->
- Feats = case Acc of
- {result, I} -> I;
- _ -> []
- end,
- {result, Feats ++ [?NS_FEATURE_MSGOFFLINE]};
-
-get_sm_features(_Acc, _From, _To, ?NS_FEATURE_MSGOFFLINE, _Lang) ->
- %% override all lesser features...
- {result, []};
-
-get_sm_features(Acc, _From, _To, _Node, _Lang) ->
- Acc.
-
-
store_packet(From, To, Packet) ->
Type = xml:get_tag_attr_s("type", Packet),
if
(Type /= "error") and (Type /= "groupchat") and
(Type /= "headline") ->
- case check_event_chatstates(From, To, Packet) of
+ case check_event(From, To, Packet) of
true ->
#jid{luser = LUser, lserver = LServer} = To,
TimeStamp = now(),
@@ -267,22 +247,12 @@ store_packet(From, To, Packet) ->
ok
end.
-%% Check if the packet has any content about XEP-0022 or XEP-0085
-check_event_chatstates(From, To, Packet) ->
+check_event(From, To, Packet) ->
{xmlelement, Name, Attrs, Els} = Packet,
- case find_x_event_chatstates(Els, {false, false, false}) of
- %% There wasn't any x:event or chatstates subelements
- {false, false, _} ->
- true;
- %% There a chatstates subelement and other stuff, but no x:event
- {false, CEl, true} when CEl /= false ->
+ case find_x_event(Els) of
+ false ->
true;
- %% There was only a subelement: a chatstates
- {false, CEl, false} when CEl /= false ->
- %% Don't allow offline storage
- false;
- %% There was an x:event element, and maybe also other stuff
- {El, _, _} when El /= false ->
+ El ->
case xml:get_subtag(El, "id") of
false ->
case xml:get_subtag(El, "offline") of
@@ -310,19 +280,16 @@ check_event_chatstates(From, To, Packet) ->
end
end.
-%% Check if the packet has subelements about XEP-0022, XEP-0085 or other
-find_x_event_chatstates([], Res) ->
- Res;
-find_x_event_chatstates([{xmlcdata, _} | Els], Res) ->
- find_x_event_chatstates(Els, Res);
-find_x_event_chatstates([El | Els], {A, B, C}) ->
+find_x_event([]) ->
+ false;
+find_x_event([{xmlcdata, _} | Els]) ->
+ find_x_event(Els);
+find_x_event([El | Els]) ->
case xml:get_tag_attr_s("xmlns", El) of
?NS_EVENT ->
- find_x_event_chatstates(Els, {El, B, C});
- ?NS_CHATSTATES ->
- find_x_event_chatstates(Els, {A, El, C});
+ El;
_ ->
- find_x_event_chatstates(Els, {A, B, true})
+ find_x_event(Els)
end.
find_x_expire(_, []) ->
@@ -372,13 +339,6 @@ resend_offline_messages(User, Server) ->
Els ++
[jlib:timestamp_to_xml(
calendar:now_to_universal_time(
- R#offline_msg.timestamp),
- utc,
- jlib:make_jid("", Server, ""),
- "Offline Storage"),
- %% TODO: Delete the next three lines once XEP-0091 is Obsolete
- jlib:timestamp_to_xml(
- calendar:now_to_universal_time(
R#offline_msg.timestamp))]}}
end,
lists:keysort(#offline_msg.timestamp, Rs));
@@ -896,8 +856,24 @@ webadmin_user_parse_query(_, "removealloffline", User, Server, _Query) ->
webadmin_user_parse_query(Acc, _Action, _User, _Server, _Query) ->
Acc.
-%% Returns as integer the number of offline messages for a given user
-count_offline_messages(LUser, LServer) ->
+count_offline_messages(User, Server) ->
+ LUser = jlib:nodeprep(User),
+ LServer = jlib:nameprep(Server),
+ DBType = gen_mod:db_type(LServer, ?MODULE),
+ count_offline_messages(LUser, LServer, DBType).
+
+count_offline_messages(LUser, LServer, mnesia) ->
+ US = {LUser, LServer},
+ F = fun () ->
+ p1_mnesia:count_records(
+ offline_msg,
+ #offline_msg{us=US, _='_'})
+ end,
+ case catch mnesia:async_dirty(F) of
+ I when is_integer(I) -> I;
+ _ -> 0
+ end;
+count_offline_messages(LUser, LServer, odbc) ->
Username = ejabberd_odbc:escape(LUser),
case catch odbc_queries:count_records_where(
LServer, "spool", "where username='" ++ Username ++ "'") of
@@ -905,4 +881,8 @@ count_offline_messages(LUser, LServer) ->
list_to_integer(Res);
_ ->
0
- end.
+ end;
+
+count_offline_messages(_Acc, User, Server) ->
+ N = count_offline_messages(User, Server),
+ {stop, N}.
diff --git a/src/mod_ping.erl b/src/mod_ping.erl
index 8cb9e0173..fb1ab9c64 100644
--- a/src/mod_ping.erl
+++ b/src/mod_ping.erl
@@ -51,7 +51,7 @@
handle_info/2, code_change/3]).
%% Hook callbacks
--export([iq_ping/3, user_online/3, user_offline/3, user_send/3]).
+-export([iq_ping/3, user_online/3, user_offline/3, user_send/4]).
-record(state, {host = "",
send_pings = ?DEFAULT_SEND_PINGS,
@@ -109,6 +109,8 @@ init([Host, Opts]) ->
?MODULE, user_online, 100),
ejabberd_hooks:add(sm_remove_connection_hook, Host,
?MODULE, user_offline, 100),
+ ejabberd_hooks:add(sm_remove_migrated_connection_hook, Host,
+ ?MODULE, user_offline, 100),
ejabberd_hooks:add(user_send_packet, Host,
?MODULE, user_send, 100);
_ ->
@@ -123,6 +125,8 @@ init([Host, Opts]) ->
terminate(_Reason, #state{host = Host}) ->
ejabberd_hooks:delete(sm_remove_connection_hook, Host,
?MODULE, user_offline, 100),
+ ejabberd_hooks:delete(sm_remove_migrated_connection_hook, Host,
+ ?MODULE, user_offline, 100),
ejabberd_hooks:delete(sm_register_connection_hook, Host,
?MODULE, user_online, 100),
ejabberd_hooks:delete(user_send_packet, Host,
@@ -195,7 +199,7 @@ user_online(_SID, JID, _Info) ->
user_offline(_SID, JID, _Info) ->
stop_ping(JID#jid.lserver, JID).
-user_send(JID, _From, _Packet) ->
+user_send(_DebugFlag, JID, _From, _Packet) ->
start_ping(JID#jid.lserver, JID).
%%====================================================================
diff --git a/src/mod_privacy.hrl b/src/mod_privacy.hrl
index fa901b687..551960a6b 100644
--- a/src/mod_privacy.hrl
+++ b/src/mod_privacy.hrl
@@ -19,6 +19,8 @@
%%%
%%%----------------------------------------------------------------------
+-define(mod_privacy_hrl, true).
+
-record(privacy, {us,
default = none,
lists = []}).
diff --git a/src/mod_proxy65/mod_proxy65_sm.erl b/src/mod_proxy65/mod_proxy65_sm.erl
index bdd3297d8..1d6a50a7f 100644
--- a/src/mod_proxy65/mod_proxy65_sm.erl
+++ b/src/mod_proxy65/mod_proxy65_sm.erl
@@ -71,7 +71,9 @@ start_link(Host, Opts) ->
gen_server:start_link({local, Proc}, ?MODULE, [Opts], []).
init([Opts]) ->
+ update_tables(),
mnesia:create_table(bytestream, [{ram_copies, [node()]},
+ {local_content, true},
{attributes, record_info(fields, bytestream)}]),
mnesia:add_table_copy(bytestream, node(), ram_copies),
MaxConnections = gen_mod:get_opt(max_connections, Opts, infinity),
@@ -179,3 +181,11 @@ activate_stream(SHA1, IJid, TJid, Host) when is_list(SHA1) ->
_ ->
error
end.
+
+update_tables() ->
+ case catch mnesia:table_info(bytestream, local_content) of
+ false ->
+ mnesia:delete_table(bytestream);
+ _ ->
+ ok
+ end.
diff --git a/src/mod_pubsub/mod_pubsub.erl b/src/mod_pubsub/mod_pubsub.erl
index fcf7de96b..33aa7a0bf 100644
--- a/src/mod_pubsub/mod_pubsub.erl
+++ b/src/mod_pubsub/mod_pubsub.erl
@@ -78,6 +78,7 @@
%% exports for console debug manual use
-export([create_node/5,
+ create_node/7,
delete_node/3,
subscribe_node/5,
unsubscribe_node/5,
@@ -87,7 +88,6 @@
get_items/2,
get_item/3,
get_cached_item/2,
- broadcast_stanza/9,
get_configure/5,
set_configure/5,
tree_action/3,
@@ -480,6 +480,55 @@ update_state_database(_Host, _ServerHost) ->
?ERROR_MSG("Problem updating Pubsub state tables:~n~p",
[Reason])
end;
+ [stateid, items, affiliation, subscriptions] ->
+ ?INFO_MSG("upgrade state pubsub table", []),
+ F = fun ({pubsub_state, {JID, Nidx}, Items, Aff, Subs}, Acc) ->
+ NewState = #pubsub_state{stateid = {JID, Nidx},
+ nodeidx = Nidx,
+ items = Items,
+ affiliation = Aff,
+ subscriptions = Subs},
+ [NewState | Acc]
+ end,
+ {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3,
+ [F, [], pubsub_state]),
+ {atomic, ok} = mnesia:delete_table(pubsub_state),
+ {atomic, ok} = mnesia:create_table(pubsub_state,
+ [{disc_copies, [node()]},
+ {attributes, record_info(fields, pubsub_state)}]),
+ FNew = fun () ->
+ lists:foreach(fun mnesia:write/1, NewRecs)
+ end,
+ case mnesia:transaction(FNew) of
+ {atomic, Res1} ->
+ ?INFO_MSG("Pubsub state tables updated correctly: ~p", [Res1]);
+ {aborted, Rea1} ->
+ ?ERROR_MSG("Problem updating Pubsub state table:~n~p", [Rea1])
+ end,
+ ?INFO_MSG("upgrade item pubsub table", []),
+ F = fun ({pubsub_item, {ItemId, Nidx}, C, M, P}, Acc) ->
+ NewItem = #pubsub_item{itemid = {ItemId, Nidx},
+ nodeidx = Nidx,
+ creation = C,
+ modification = M,
+ payload = P},
+ [NewItem | Acc]
+ end,
+ {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3,
+ [F, [], pubsub_item]),
+ {atomic, ok} = mnesia:delete_table(pubsub_item),
+ {atomic, ok} = mnesia:create_table(pubsub_item,
+ [{disc_copies, [node()]},
+ {attributes, record_info(fields, pubsub_item)}]),
+ FNew = fun () ->
+ lists:foreach(fun mnesia:write/1, NewRecs)
+ end,
+ case mnesia:transaction(FNew) of
+ {atomic, Res2} ->
+ ?INFO_MSG("Pubsub item tables updated correctly: ~p", [Res2]);
+ {aborted, Rea2} ->
+ ?ERROR_MSG("Problem updating Pubsub item table:~n~p", [Rea2])
+ end;
_ ->
ok
end.
@@ -1760,10 +1809,8 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) ->
{result, true} ->
case tree_call(Host, create_node, [Host, Node, Type, Owner, NodeOptions, Parents]) of
{ok, NodeId} ->
- ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, Owner]),
- SubsByDepth = [{Depth, [{N, get_node_subs(N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree],
case node_call(Type, create_node, [NodeId, Owner]) of
- {result, Result} -> {result, {NodeId, SubsByDepth, Result}};
+ {result, Result} -> {result, {NodeId, Result}};
Error -> Error
end;
{error, {virtual, NodeId}} ->
@@ -1782,17 +1829,17 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) ->
[{xmlelement, "create", nodeAttr(Node),
[]}]}],
case transaction(CreateNode, transaction) of
- {result, {NodeId, SubsByDepth, {Result, broadcast}}} ->
- broadcast_created_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth),
+ {result, {NodeId, {Result, broadcast}}} ->
+ broadcast_created_node(Host, Node, NodeId, Type, NodeOptions),
ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
- {result, {NodeId, _SubsByDepth, default}} ->
+ {result, {NodeId, default}} ->
ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]),
{result, Reply};
- {result, {NodeId, _SubsByDepth, Result}} ->
+ {result, {NodeId, Result}} ->
ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]),
{result, Result};
Error ->
@@ -1825,11 +1872,9 @@ delete_node(Host, Node, Owner) ->
Action = fun(#pubsub_node{type = Type, id = NodeId}) ->
case node_call(Type, get_affiliation, [NodeId, Owner]) of
{result, owner} ->
- ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]),
- SubsByDepth = [{Depth, [{N, get_node_subs(N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree],
Removed = tree_call(Host, delete_node, [Host, Node]),
case node_call(Type, delete_node, [Removed]) of
- {result, Res} -> {result, {SubsByDepth, Res}};
+ {result, Res} -> {result, Res};
Error -> Error
end;
_ ->
@@ -1840,20 +1885,20 @@ delete_node(Host, Node, Owner) ->
Reply = [],
ServerHost = get(server_host), % not clean, but prevent many API changes
case transaction(Host, Node, Action, transaction) of
- {result, {_TNode, {SubsByDepth, {Result, broadcast, Removed}}}} ->
+ {result, {_, {Result, broadcast, Removed}}} ->
lists:foreach(fun({RNode, _RSubscriptions}) ->
{RH, RN} = RNode#pubsub_node.nodeid,
NodeId = RNode#pubsub_node.id,
Type = RNode#pubsub_node.type,
Options = RNode#pubsub_node.options,
- broadcast_removed_node(RH, RN, NodeId, Type, Options, SubsByDepth),
+ broadcast_removed_node(RH, RN, NodeId, Type, Options),
ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, RH, RN, NodeId])
end, Removed),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
- {result, {_TNode, {_, {Result, Removed}}}} ->
+ {result, {_, {Result, Removed}}} ->
lists:foreach(fun({RNode, _RSubscriptions}) ->
{RH, RN} = RNode#pubsub_node.nodeid,
NodeId = RNode#pubsub_node.id,
@@ -1863,11 +1908,11 @@ delete_node(Host, Node, Owner) ->
default -> {result, Reply};
_ -> {result, Result}
end;
- {result, {TNode, {_, default}}} ->
+ {result, {TNode, default}} ->
NodeId = TNode#pubsub_node.id,
ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, Host, Node, NodeId]),
{result, Reply};
- {result, {TNode, {_, Result}}} ->
+ {result, {TNode, Result}} ->
NodeId = TNode#pubsub_node.id,
ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, Host, Node, NodeId]),
{result, Result};
@@ -2070,7 +2115,6 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) ->
node_call(Type, publish_item, [NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload])
end
end,
- ejabberd_hooks:run(pubsub_publish_item, ServerHost, [ServerHost, Node, Publisher, service_jid(Host), ItemId, Payload]),
Reply = [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}],
[{xmlelement, "publish", nodeAttr(Node),
[{xmlelement, "item", itemAttr(ItemId), []}]}]}],
@@ -2079,20 +2123,16 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) ->
NodeId = TNode#pubsub_node.id,
Type = TNode#pubsub_node.type,
Options = TNode#pubsub_node.options,
+ BrPayload = case Broadcast of
+ broadcast -> Payload;
+ PluginPayload -> PluginPayload
+ end,
+ ejabberd_hooks:run(pubsub_publish_item, ServerHost, [ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]),
+ set_cached_item(Host, NodeId, ItemId, Publisher, BrPayload),
case get_option(Options, deliver_notifications) of
- true ->
- BroadcastPayload = case Broadcast of
- default -> Payload;
- broadcast -> Payload;
- PluginPayload -> PluginPayload
- end,
- broadcast_publish_item(Host, Node, NodeId, Type, Options,
- Removed, ItemId, jlib:jid_tolower(Publisher),
- BroadcastPayload);
- false ->
- ok
- end,
- set_cached_item(Host, NodeId, ItemId, Publisher, Payload),
+ true -> broadcast_publish_item(Host, Node, NodeId, Type, Options, ItemId, jlib:jid_tolower(Publisher), BrPayload, Removed);
+ false -> ok
+ end,
case Result of
default -> {result, Reply};
_ -> {result, Result}
@@ -2621,14 +2661,18 @@ get_options_helper(JID, Lang, Node, NodeID, SubID, Type) ->
read_sub(Subscriber, Node, NodeID, SubID, Lang) ->
case pubsub_subscription:get_subscription(Subscriber, NodeID, SubID) of
- {error, notfound} ->
- {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")};
{result, #pubsub_subscription{options = Options}} ->
{result, XdataEl} = pubsub_subscription:get_options_xform(Lang, Options),
OptionsEl = {xmlelement, "options", [{"jid", jlib:jid_to_string(Subscriber)},
{"subid", SubID}|nodeAttr(Node)],
[XdataEl]},
PubsubEl = {xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], [OptionsEl]},
+ {result, PubsubEl};
+ _ ->
+ OptionsEl = {xmlelement, "options", [{"jid", jlib:jid_to_string(Subscriber)},
+ {"subid", SubID}|nodeAttr(Node)],
+ []},
+ PubsubEl = {xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], [OptionsEl]},
{result, PubsubEl}
end.
@@ -2678,12 +2722,14 @@ set_options_helper(Configuration, JID, NodeID, SubID, Type) ->
write_sub(_Subscriber, _NodeID, _SubID, invalid) ->
{error, extended_error(?ERR_BAD_REQUEST, "invalid-options")};
+write_sub(_Subscriber, _NodeID, _SubID, []) ->
+ {result, []};
write_sub(Subscriber, NodeID, SubID, Options) ->
case pubsub_subscription:set_subscription(Subscriber, NodeID, SubID, Options) of
- {error, notfound} ->
- {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")};
{result, _} ->
- {result, []}
+ {result, []};
+ {error, _} ->
+ {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}
end.
%% @spec (Host, Node, JID, Plugins) -> {error, Reason} | {result, Response}
@@ -2977,21 +3023,20 @@ sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) ->
sub_option_can_deliver(NotifyType, Depth, Option)
end, SubOptions).
+node_to_deliver(LJID, NodeOptions) ->
+ presence_can_deliver(LJID, get_option(NodeOptions, presence_based_delivery)).
+
sub_option_can_deliver(items, _, {subscription_type, nodes}) -> false;
sub_option_can_deliver(nodes, _, {subscription_type, items}) -> false;
sub_option_can_deliver(_, _, {subscription_depth, all}) -> true;
sub_option_can_deliver(_, Depth, {subscription_depth, D}) -> Depth =< D;
-sub_option_can_deliver(_, _, {deliver, false}) -> false;
-sub_option_can_deliver(_, _, {expire, When}) -> now() < When;
-sub_option_can_deliver(_, _, _) -> true.
-
-node_to_deliver(LJID, NodeOptions) ->
- PresenceDelivery = get_option(NodeOptions, presence_based_delivery),
- presence_can_deliver(LJID, PresenceDelivery).
+sub_option_can_deliver(_, _, {deliver, false}) -> false;
+sub_option_can_deliver(_, _, {expire, When}) -> now() < When;
+sub_option_can_deliver(_, _, _) -> true.
presence_can_deliver(_, false) -> true;
presence_can_deliver({User, Server, Resource}, true) ->
- case mnesia:dirty_match_object({session, '_', '_', {User, Server}, '_', '_'}) of
+ case ejabberd_sm:get_user_sessions(User, Server) of
[] -> false;
Ss ->
lists:foldl(fun(_, true) -> true;
@@ -3009,45 +3054,45 @@ state_can_deliver({U, S, R}, []) -> [{U, S, R}];
state_can_deliver({U, S, R}, SubOptions) ->
%% Check SubOptions for 'show_values'
case lists:keysearch('show_values', 1, SubOptions) of
- %% If not in suboptions, item can be delivered, case doesn't apply
- false -> [{U, S, R}];
- %% If in a suboptions ...
- {_, {_, ShowValues}} ->
- %% Get subscriber resources
- Resources = case R of
- %% If the subscriber JID is a bare one, get all its resources
- [] -> user_resources(U, S);
- %% If the subscriber JID is a full one, use its resource
- R -> [R]
- end,
- %% For each resource, test if the item is allowed to be delivered
- %% based on resource state
- lists:foldl(
- fun(Resource, Acc) ->
- get_resource_state({U, S, Resource}, ShowValues, Acc)
- end, [], Resources)
+ %% If not in suboptions, item can be delivered, case doesn't apply
+ false -> [{U, S, R}];
+ %% If in a suboptions ...
+ {_, {_, ShowValues}} ->
+ %% Get subscriber resources
+ Resources = case R of
+ %% If the subscriber JID is a bare one, get all its resources
+ [] -> user_resources(U, S);
+ %% If the subscriber JID is a full one, use its resource
+ R -> [R]
+ end,
+ %% For each resource, test if the item is allowed to be delivered
+ %% based on resource state
+ lists:foldl(
+ fun(Resource, Acc) ->
+ get_resource_state({U, S, Resource}, ShowValues, Acc)
+ end, [], Resources)
end.
get_resource_state({U, S, R}, ShowValues, JIDs) ->
%% Get user session PID
case ejabberd_sm:get_session_pid(U, S, R) of
- %% If no PID, item can be delivered
- none -> lists:append([{U, S, R}], JIDs);
- %% If PID ...
- Pid ->
- %% Get user resource state
- %% TODO : add a catch clause
- Show = case ejabberd_c2s:get_presence(Pid) of
- {_, _, "available", _} -> "online";
- {_, _, State, _} -> State
- end,
- %% Is current resource state listed in 'show-values' suboption ?
- case lists:member(Show, ShowValues) of %andalso Show =/= "online" of
- %% If yes, item can be delivered
- true -> lists:append([{U, S, R}], JIDs);
- %% If no, item can't be delivered
- false -> JIDs
- end
+ %% If no PID, item can be delivered
+ none -> lists:append([{U, S, R}], JIDs);
+ %% If PID ...
+ Pid ->
+ %% Get user resource state
+ %% TODO : add a catch clause
+ Show = case ejabberd_c2s:get_presence(Pid) of
+ {_, _, "available", _} -> "online";
+ {_, _, State, _} -> State
+ end,
+ %% Is current resource state listed in 'show-values' suboption ?
+ case lists:member(Show, ShowValues) of %andalso Show =/= "online" of
+ %% If yes, item can be delivered
+ true -> lists:append([{U, S, R}], JIDs);
+ %% If no, item can't be delivered
+ false -> JIDs
+ end
end.
%% @spec (Payload) -> int()
@@ -3073,224 +3118,150 @@ event_stanza_withmoreels(Els, MoreEls) ->
{xmlelement, "message", [],
[{xmlelement, "event", [{"xmlns", ?NS_PUBSUB_EVENT}], Els} | MoreEls]}.
+event_stanza(Event, EvAttr) ->
+ event_stanza([{xmlelement, Event, EvAttr, []}]).
+event_stanza(Event, EvAttr, Entries) ->
+ event_stanza([{xmlelement, Event, EvAttr,
+ [{xmlelement, Entry, EnAttr, []} || {Entry, EnAttr} <- Entries]}]).
+event_stanza(Event, EvAttr, Entry, EnAttr, Payload) ->
+ event_stanza([{xmlelement, Event, EvAttr, [{xmlelement, Entry, EnAttr, Payload}]}]).
+event_stanza(Event, EvAttr, Entry, EnAttr, Payload, Publisher) ->
+ Stanza = event_stanza(Event, EvAttr, Entry, EnAttr, Payload),
+ add_extended_headers(Stanza, extended_headers([jlib:jid_to_string(Publisher)])).
+
%%%%%% broadcast functions
-broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, Removed, ItemId, From, Payload) ->
- case get_collection_subscriptions(Host, Node) of
- SubsByDepth when is_list(SubsByDepth) ->
- Content = case get_option(NodeOptions, deliver_payloads) of
- true -> Payload;
- false -> []
- end,
- Stanza = event_stanza(
- [{xmlelement, "items", nodeAttr(Node),
- [{xmlelement, "item", itemAttr(ItemId), Content}]}]),
- broadcast_stanza(Host, From, Node, NodeId, Type,
- NodeOptions, SubsByDepth, items, Stanza, true),
- case Removed of
- [] ->
- ok;
- _ ->
- case get_option(NodeOptions, notify_retract) of
- true ->
- RetractStanza = event_stanza(
- [{xmlelement, "items", nodeAttr(Node),
- [{xmlelement, "retract", itemAttr(RId), []} || RId <- Removed]}]),
- broadcast_stanza(Host, Node, NodeId, Type,
- NodeOptions, SubsByDepth,
- items, RetractStanza, true);
- _ ->
- ok
- end
- end,
- {result, true};
+broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, ItemId, Publisher, Payload, Removed) ->
+ Publish = case get_option(NodeOptions, deliver_payloads) of
+ true -> event_stanza("items", nodeAttr(Node), "item", itemAttr(ItemId), Payload, Publisher);
+ false -> event_stanza("items", nodeAttr(Node), "item", itemAttr(ItemId), [], Publisher)
+ end,
+ case Removed of
+ [] ->
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, items, true, Publish, true)};
_ ->
- {result, false}
+ Retract = event_stanza("items", nodeAttr(Node), [{"retract", itemAttr(Rid)} || Rid <- Removed]),
+ Stanzas = [{true, Publish, true}, {get_option(NodeOptions, notify_retract), Retract, true}],
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, items, Stanzas)}
end.
broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds) ->
- broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, false).
-broadcast_retract_items(_Host, _Node, _NodeId, _Type, _NodeOptions, [], _ForceNotify) ->
+ broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, notify_retract).
+broadcast_retract_items(_Host, _Node, _NodeId, _Type, _NodeOptions, [], _) ->
{result, false};
-broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, ForceNotify) ->
- case (get_option(NodeOptions, notify_retract) or ForceNotify) of
- true ->
- case get_collection_subscriptions(Host, Node) of
- SubsByDepth when is_list(SubsByDepth) ->
- Stanza = event_stanza(
- [{xmlelement, "items", nodeAttr(Node),
- [{xmlelement, "retract", itemAttr(ItemId), []} || ItemId <- ItemIds]}]),
- broadcast_stanza(Host, Node, NodeId, Type,
- NodeOptions, SubsByDepth, items, Stanza, true),
- {result, true};
- _ ->
- {result, false}
- end;
- _ ->
- {result, false}
- end.
+broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, false) ->
+ broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, notify_retract);
+broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, Notify) ->
+ Stanza = event_stanza("items", nodeAttr(Node), [{"retract", itemAttr(Rid)} || Rid <- ItemIds]),
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, items, Notify, Stanza, true)}.
broadcast_purge_node(Host, Node, NodeId, Type, NodeOptions) ->
- case get_option(NodeOptions, notify_retract) of
- true ->
- case get_collection_subscriptions(Host, Node) of
- SubsByDepth when is_list(SubsByDepth) ->
- Stanza = event_stanza(
- [{xmlelement, "purge", nodeAttr(Node),
- []}]),
- broadcast_stanza(Host, Node, NodeId, Type,
- NodeOptions, SubsByDepth, nodes, Stanza, false),
- {result, true};
- _ ->
- {result, false}
- end;
- _ ->
- {result, false}
- end.
+ Stanza = event_stanza("purge", nodeAttr(Node)),
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, nodes, notify_retract, Stanza, false)}.
-broadcast_removed_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth) ->
- case get_option(NodeOptions, notify_delete) of
- true ->
- case SubsByDepth of
- [] ->
- {result, false};
- _ ->
- Stanza = event_stanza(
- [{xmlelement, "delete", nodeAttr(Node),
- []}]),
- broadcast_stanza(Host, Node, NodeId, Type,
- NodeOptions, SubsByDepth, nodes, Stanza, false),
- {result, true}
- end;
- _ ->
- {result, false}
- end.
+broadcast_removed_node(Host, Node, NodeId, Type, NodeOptions) ->
+ Stanza = event_stanza("delete", nodeAttr(Node)),
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, nodes, notify_delete, Stanza, false)}.
-broadcast_created_node(_, _, _, _, _, []) ->
- {result, false};
-broadcast_created_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth) ->
- Stanza = event_stanza([{xmlelement, "create", nodeAttr(Node), []}]),
- broadcast_stanza(Host, Node, NodeId, Type, NodeOptions, SubsByDepth, nodes, Stanza, true),
- {result, true}.
+broadcast_created_node(Host, Node, NodeId, Type, NodeOptions) ->
+ Stanza = event_stanza("create", nodeAttr(Node)),
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, nodes, true, Stanza, true)}.
broadcast_config_notification(Host, Node, NodeId, Type, NodeOptions, Lang) ->
- case get_option(NodeOptions, notify_config) of
+ Stanza = case get_option(NodeOptions, deliver_payloads) of
true ->
- case get_collection_subscriptions(Host, Node) of
- SubsByDepth when is_list(SubsByDepth) ->
- Content = case get_option(NodeOptions, deliver_payloads) of
- true ->
- [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}],
- get_configure_xfields(Type, NodeOptions, Lang, [])}];
- false ->
- []
+ event_stanza("configuration", nodeAttr(Node),
+ "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}],
+ get_configure_xfields(Type, NodeOptions, Lang, []));
+ false ->
+ event_stanza("configuration", nodeAttr(Node))
+ end,
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, nodes, notify_config, Stanza, false)}.
+
+broadcast(Host, Node, NodeId, Type, NodeOptions, Notify, Stanzas) ->
+ Subs = node_subscriptions(Host, Node, NodeId, Type, NodeOptions, Notify),
+ Result = [broadcast(Host, Node, NodeId, Type, NodeOptions, Subs, Stanza, SHIM) ||
+ {Cond, Stanza, SHIM} <- Stanzas, Cond =:= true],
+ lists:member(true, Result).
+broadcast(Host, Node, NodeId, Type, NodeOptions, Notify, true, Stanza, SHIM) ->
+ Subs = node_subscriptions(Host, Node, NodeId, Type, NodeOptions, Notify),
+ broadcast(Host, Node, NodeId, Type, NodeOptions, Subs, Stanza, SHIM);
+broadcast(_Host, _Node, _NodeId, _Type, _NodeOptions, _Notify, false, _Stanza, _SHIM) ->
+ false;
+broadcast(Host, Node, NodeId, Type, NodeOptions, Notify, Condition, Stanza, SHIM) ->
+ broadcast(Host, Node, NodeId, Type, NodeOptions, Notify, get_option(NodeOptions, Condition), Stanza, SHIM).
+
+broadcast({U, S, R}, Node, NodeId, Type, NodeOptions, Subscriptions, Stanza, SHIM) ->
+ broadcast(S, Node, NodeId, Type, NodeOptions, Subscriptions, Stanza, SHIM)
+ or case ejabberd_sm:get_session_pid(U, S, user_resource(U, S, R)) of
+ C2SPid when is_pid(C2SPid) ->
+ %% 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
+ Event = {pep_message, binary_to_list(Node)++"+notify"},
+ Message = case get_option(NodeOptions, notification_type, headline) of
+ normal -> Stanza;
+ MsgType -> add_message_type(Stanza, atom_to_list(MsgType))
+ end,
+ ejabberd_c2s:broadcast(C2SPid, Event, jlib:make_jid(U, S, ""), Message),
+ true;
+ _ ->
+ ?DEBUG("~p@~p has no session; can't deliver stanza: ~p", [U, S, Stanza]),
+ false
+ end;
+broadcast(_Host, _Node, _NodeId, _Type, _NodeOptions, [], _Stanza, _SHIM) ->
+ false;
+broadcast(Host, _Node, _NodeId, _Type, NodeOptions, Subscriptions, Stanza, SHIM) ->
+ From = service_jid(Host),
+ Message = case get_option(NodeOptions, notification_type, headline) of
+ normal -> Stanza;
+ MsgType -> add_message_type(Stanza, atom_to_list(MsgType))
+ end,
+ lists:foreach(fun({LJID, NodeName, SubIds}) ->
+ Send = case {SHIM, SubIds} of
+ {false, _} -> Message;
+ {true, [_]} -> add_shim_headers(Message, collection_shim(NodeName));
+ {true, _} -> add_shim_headers(Message, lists:append(collection_shim(NodeName), subid_shim(SubIds)))
end,
- Stanza = event_stanza(
- [{xmlelement, "configuration", nodeAttr(Node), Content}]),
- broadcast_stanza(Host, Node, NodeId, Type,
- NodeOptions, SubsByDepth, nodes, Stanza, false),
- {result, true};
- _ ->
- {result, false}
- end;
+ ejabberd_router:route(From, jlib:make_jid(LJID), Send)
+ end, Subscriptions),
+ true.
+
+node_subscriptions(Host, Node, NodeId, Type, _NodeOptions, Notify) ->
+ % TODO temporary dirty condition, should be improved using plugin or node options
+ case Type of
+ "flat" -> node_subscriptions_bare(Host, Node, NodeId, Type);
+ "pep" -> node_subscriptions_bare(Host, Node, NodeId, Type);
+ _ -> node_subscriptions_full(Host, Node, Notify)
+ end.
+
+node_subscriptions_bare(Host, Node, NodeId, Type) ->
+ case node_action(Host, Type, get_node_subscriptions, [NodeId]) of
+ {result, Subs} ->
+ SubsByJid = lists:foldl(
+ fun({JID, subscribed, SubId}, Acc) ->
+ case dict:is_key(JID, Acc) of
+ true -> dict:append(JID, SubId, Acc);
+ false -> dict:store(JID, [SubId], Acc)
+ end;
+ (_, Acc) ->
+ Acc
+ end, dict:new(), Subs),
+ [{J, Node, S} || {J, S} <- dict:to_list(SubsByJid)];
_ ->
- {result, false}
+ []
end.
-get_collection_subscriptions(Host, Node) ->
+node_subscriptions_full(Host, Node, NotifyType) ->
Action = fun() ->
- {result, lists:map(fun({Depth, Nodes}) ->
- {Depth, [{N, get_node_subs(N)} || N <- Nodes]}
- end, tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]))}
+ Collection = tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]),
+ {result, [{Depth, [{N, sub_with_options(N)} || N <- Nodes]} || {Depth, Nodes} <- Collection]}
end,
case transaction(Action, sync_dirty) of
- {result, CollSubs} -> CollSubs;
+ {result, CollSubs} -> subscribed_nodes_by_jid(NotifyType, CollSubs);
_ -> []
end.
-get_node_subs(#pubsub_node{type = Type,
- id = NodeID}) ->
- case node_call(Type, get_node_subscriptions, [NodeID]) of
- {result, Subs} -> get_options_for_subs(NodeID, Subs);
- Other -> Other
- end.
-
-get_options_for_subs(NodeID, Subs) ->
- lists:foldl(fun({JID, subscribed, SubID}, Acc) ->
- case pubsub_subscription:read_subscription(JID, NodeID, SubID) of
- {error, notfound} -> [{JID, SubID, []} | Acc];
- #pubsub_subscription{options = Options} -> [{JID, SubID, Options} | Acc];
- _ -> Acc
- end;
- (_, Acc) ->
- Acc
- end, [], Subs).
-
-broadcast_stanza(Host, _Node, _NodeId, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) ->
- 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, atom_to_list(MsgType))
- end,
- %% Handles explicit subscriptions
- SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth),
- lists:foreach(fun ({LJID, NodeName, SubIDs}) ->
- LJIDs = case BroadcastAll of
- true ->
- {U, S, _} = LJID,
- [{U, S, R} || R <- user_resources(U, S)];
- false ->
- [LJID]
- end,
- %% Determine if the stanza should have SHIM ('SubID' and 'name') headers
- StanzaToSend = case {SHIM, SubIDs} of
- {false, _} ->
- Stanza;
- %% If there's only one SubID, don't add it
- {true, [_]} ->
- add_shim_headers(Stanza, collection_shim(NodeName));
- {true, SubIDs} ->
- add_shim_headers(Stanza, lists:append(collection_shim(NodeName), subid_shim(SubIDs)))
- end,
- lists:foreach(fun(To) ->
- ejabberd_router:route(From, jlib:make_jid(To), StanzaToSend)
- end, LJIDs)
- end, SubIDsByJID).
-
-broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) ->
- broadcast_stanza({LUser, LServer, LResource}, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM),
- %% Handles implicit presence subscriptions
- SenderResource = case LResource of
- [] ->
- case user_resources(LUser, LServer) of
- [Resource|_] -> Resource;
- _ -> ""
- end;
- _ ->
- LResource
- end,
- 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, atom_to_list(MsgType))
- end,
- %% 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
- ejabberd_c2s:broadcast(C2SPid,
- {pep_message, binary_to_list(Node)++"+notify"},
- _Sender = jlib:make_jid(LUser, LServer, ""),
- _StanzaToSend = add_extended_headers(Stanza,
- _ReplyTo = extended_headers([jlib:jid_to_string(Publisher)])));
- _ ->
- ?DEBUG("~p@~p has no session; can't deliver ~p to contacts", [LUser, LServer, BaseStanza])
- end;
-broadcast_stanza(Host, _Publisher, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) ->
- broadcast_stanza(Host, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM).
-
subscribed_nodes_by_jid(NotifyType, SubsByDepth) ->
NodesToDeliver = fun(Depth, Node, Subs, Acc) ->
NodeName = case Node#pubsub_node.nodeid of
@@ -3300,7 +3271,7 @@ subscribed_nodes_by_jid(NotifyType, SubsByDepth) ->
NodeOptions = Node#pubsub_node.options,
lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) ->
case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of
- true ->
+ true ->
%% If is to deliver :
case state_can_deliver(LJID, SubOptions) of
[] -> {JIDs, Recipients};
@@ -3309,13 +3280,13 @@ subscribed_nodes_by_jid(NotifyType, SubsByDepth) ->
fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) ->
case lists:member(JIDToDeliver, JIDs) of
%% check if the JIDs co-accumulator contains the Subscription Jid,
- false ->
+ false ->
%% - if not,
%% - add the Jid to JIDs list co-accumulator ;
%% - create a tuple of the Jid, NodeId, and SubID (as list),
%% and add the tuple to the Recipients list co-accumulator
{[JIDToDeliver | JIDsAcc], [{JIDToDeliver, NodeName, [SubID]} | RecipientsAcc]};
- true ->
+ true ->
%% - if the JIDs co-accumulator contains the Jid
%% get the tuple containing the Jid from the Recipient list co-accumulator
{_, {JIDToDeliver, NodeName1, SubIDs}} = lists:keysearch(JIDToDeliver, 1, RecipientsAcc),
@@ -3344,9 +3315,33 @@ subscribed_nodes_by_jid(NotifyType, SubsByDepth) ->
{_, JIDSubs} = lists:foldl(DepthsToDeliver, {[], []}, SubsByDepth),
JIDSubs.
+sub_with_options(#pubsub_node{type = Type, id = NodeId}) ->
+ case node_call(Type, get_node_subscriptions, [NodeId]) of
+ {result, Subs} ->
+ lists:foldl(
+ fun({JID, subscribed, SubId}, Acc) -> [sub_with_options(JID, NodeId, SubId) | Acc];
+ (_, Acc) -> Acc
+ end, [], Subs);
+ _ ->
+ []
+ end.
+sub_with_options(JID, NodeId, SubId) ->
+ case pubsub_subscription:read_subscription(JID, NodeId, SubId) of
+ #pubsub_subscription{options = Options} -> {JID, SubId, Options};
+ _ -> {JID, SubId, []}
+ end.
+
user_resources(User, Server) ->
ejabberd_sm:get_user_resources(User, Server).
+user_resource(User, Server, []) ->
+ case user_resources(User, Server) of
+ [R|_] -> R;
+ _ -> []
+ end;
+user_resource(_, _, Resource) ->
+ Resource.
+
%%%%%%% Configuration handling
%%<p>There are several reasons why the default node configuration options request might fail:</p>
@@ -3949,7 +3944,7 @@ extended_headers(Jids) ->
on_user_offline(_, JID, _) ->
{User, Server, Resource} = jlib:jid_tolower(JID),
- case ejabberd_sm:get_user_resources(User, Server) of
+ case user_resources(User, Server) of
[] -> purge_offline({User, Server, Resource});
_ -> true
end.
diff --git a/src/mod_pubsub/mod_pubsub_odbc.erl b/src/mod_pubsub/mod_pubsub_odbc.erl
index 6d1c9ed26..edf74458c 100644
--- a/src/mod_pubsub/mod_pubsub_odbc.erl
+++ b/src/mod_pubsub/mod_pubsub_odbc.erl
@@ -78,6 +78,7 @@
%% exports for console debug manual use
-export([create_node/5,
+ create_node/7,
delete_node/3,
subscribe_node/5,
unsubscribe_node/5,
@@ -87,7 +88,6 @@
get_items/2,
get_item/3,
get_cached_item/2,
- broadcast_stanza/9,
get_configure/5,
set_configure/5,
tree_action/3,
@@ -637,7 +637,7 @@ remove_user(User, Server) ->
{result, Affiliations} = node_action(Host, PType, get_entity_affiliations, [Host, Entity]),
lists:foreach(fun
({#pubsub_node{nodeid = {H, N}, parents = []}, owner}) -> delete_node(H, N, Entity);
- ({#pubsub_node{nodeid = {H, N}, type = "hometree"}, owner}) when N == HomeTreeBase -> delete_node(H, N, Entity);
+ ({#pubsub_node{nodeid = {H, N}, type = "hometree_odbc"}, owner}) when N == HomeTreeBase -> delete_node(H, N, Entity);
({#pubsub_node{id = NodeId}, publisher}) -> node_action(Host, PType, set_affiliation, [NodeId, Entity, none]);
(_) -> ok
end, Affiliations)
@@ -1575,10 +1575,8 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) ->
{result, true} ->
case tree_call(Host, create_node, [Host, Node, Type, Owner, NodeOptions, Parents]) of
{ok, NodeId} ->
- ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, Owner]),
- SubsByDepth = [{Depth, [{N, get_node_subs(N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree],
case node_call(Type, create_node, [NodeId, Owner]) of
- {result, Result} -> {result, {NodeId, SubsByDepth, Result}};
+ {result, Result} -> {result, {NodeId, Result}};
Error -> Error
end;
{error, {virtual, NodeId}} ->
@@ -1597,17 +1595,17 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) ->
[{xmlelement, "create", nodeAttr(Node),
[]}]}],
case transaction(Host, CreateNode, transaction) of
- {result, {NodeId, SubsByDepth, {Result, broadcast}}} ->
- broadcast_created_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth),
+ {result, {NodeId, {Result, broadcast}}} ->
+ broadcast_created_node(Host, Node, NodeId, Type, NodeOptions),
ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
- {result, {NodeId, _SubsByDepth, default}} ->
+ {result, {NodeId, default}} ->
ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]),
{result, Reply};
- {result, {NodeId, _SubsByDepth, Result}} ->
+ {result, {NodeId, Result}} ->
ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]),
{result, Result};
Error ->
@@ -1640,11 +1638,9 @@ delete_node(Host, Node, Owner) ->
Action = fun(#pubsub_node{type = Type, id = NodeId}) ->
case node_call(Type, get_affiliation, [NodeId, Owner]) of
{result, owner} ->
- ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]),
- SubsByDepth = [{Depth, [{N, get_node_subs(N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree],
Removed = tree_call(Host, delete_node, [Host, Node]),
case node_call(Type, delete_node, [Removed]) of
- {result, Res} -> {result, {SubsByDepth, Res}};
+ {result, Res} -> {result, Res};
Error -> Error
end;
_ ->
@@ -1655,20 +1651,20 @@ delete_node(Host, Node, Owner) ->
Reply = [],
ServerHost = get(server_host), % not clean, but prevent many API changes
case transaction(Host, Node, Action, transaction) of
- {result, {_TNode, {SubsByDepth, {Result, broadcast, Removed}}}} ->
+ {result, {_, {Result, broadcast, Removed}}} ->
lists:foreach(fun({RNode, _RSubscriptions}) ->
{RH, RN} = RNode#pubsub_node.nodeid,
NodeId = RNode#pubsub_node.id,
Type = RNode#pubsub_node.type,
Options = RNode#pubsub_node.options,
- broadcast_removed_node(RH, RN, NodeId, Type, Options, SubsByDepth),
+ broadcast_removed_node(RH, RN, NodeId, Type, Options),
ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, RH, RN, NodeId])
end, Removed),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
- {result, {_TNode, {_, {Result, Removed}}}} ->
+ {result, {_, {Result, Removed}}} ->
lists:foreach(fun({RNode, _RSubscriptions}) ->
{RH, RN} = RNode#pubsub_node.nodeid,
NodeId = RNode#pubsub_node.id,
@@ -1678,11 +1674,11 @@ delete_node(Host, Node, Owner) ->
default -> {result, Reply};
_ -> {result, Result}
end;
- {result, {TNode, {_, default}}} ->
+ {result, {TNode, default}} ->
NodeId = TNode#pubsub_node.id,
ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, Host, Node, NodeId]),
{result, Reply};
- {result, {TNode, {_, Result}}} ->
+ {result, {TNode, Result}} ->
NodeId = TNode#pubsub_node.id,
ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, Host, Node, NodeId]),
{result, Result};
@@ -1852,9 +1848,12 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) ->
Features = features(Type),
PublishFeature = lists:member("publish", Features),
PublishModel = get_option(Options, publish_model),
- MaxItems = max_items(Host, Options),
DeliverPayloads = get_option(Options, deliver_payloads),
PersistItems = get_option(Options, persist_items),
+ MaxItems = case PersistItems of
+ false -> 0;
+ true -> max_items(Host, Options)
+ end,
PayloadCount = payload_xmlelements(Payload),
PayloadSize = size(term_to_binary(Payload))-2, % size(term_to_binary([])) == 2
PayloadMaxSize = get_option(Options, max_payload_size),
@@ -1883,7 +1882,6 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) ->
node_call(Type, publish_item, [NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload])
end
end,
- ejabberd_hooks:run(pubsub_publish_item, ServerHost, [ServerHost, Node, Publisher, service_jid(Host), ItemId, Payload]),
Reply = [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}],
[{xmlelement, "publish", nodeAttr(Node),
[{xmlelement, "item", itemAttr(ItemId), []}]}]}],
@@ -1892,20 +1890,16 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) ->
NodeId = TNode#pubsub_node.id,
Type = TNode#pubsub_node.type,
Options = TNode#pubsub_node.options,
+ BrPayload = case Broadcast of
+ broadcast -> Payload;
+ PluginPayload -> PluginPayload
+ end,
+ ejabberd_hooks:run(pubsub_publish_item, ServerHost, [ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]),
+ set_cached_item(Host, NodeId, ItemId, Publisher, BrPayload),
case get_option(Options, deliver_notifications) of
- true ->
- BroadcastPayload = case Broadcast of
- default -> Payload;
- broadcast -> Payload;
- PluginPayload -> PluginPayload
- end,
- broadcast_publish_item(Host, Node, NodeId, Type, Options,
- Removed, ItemId, jlib:jid_tolower(Publisher),
- BroadcastPayload);
- false ->
- ok
- end,
- set_cached_item(Host, NodeId, ItemId, Publisher, Payload),
+ true -> broadcast_publish_item(Host, Node, NodeId, Type, Options, ItemId, jlib:jid_tolower(Publisher), BrPayload, Removed);
+ false -> ok
+ end,
case Result of
default -> {result, Reply};
_ -> {result, Result}
@@ -2164,7 +2158,7 @@ get_allowed_items_call(Host, NodeIdx, From, Type, Options, Owners, RSM) ->
%% 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, {U,S,R} = 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
@@ -2185,8 +2179,21 @@ send_items(Host, Node, NodeId, Type, LJID, last) ->
[{xmlelement, "items", nodeAttr(Node),
itemsEls([LastItem])}], ModifNow, ModifUSR)
end,
- ejabberd_router:route(service_jid(Host), jlib:make_jid(LJID), Stanza);
-send_items(Host, Node, NodeId, Type, LJID, Number) ->
+ case is_tuple(Host) of
+ false ->
+ ejabberd_router:route(service_jid(Host), jlib:make_jid(LJID), Stanza);
+ true ->
+ case ejabberd_sm:get_session_pid(U,S,R) of
+ C2SPid when is_pid(C2SPid) ->
+ ejabberd_c2s:broadcast(C2SPid,
+ {pep_message, binary_to_list(Node)++"+notify"},
+ _Sender = service_jid(Host),
+ Stanza);
+ _ ->
+ ok
+ end
+ end;
+send_items(Host, Node, NodeId, Type, {U,S,R} = LJID, Number) ->
ToSend = case node_action(Host, Type, get_items, [NodeId, LJID]) of
{result, []} ->
[];
@@ -2209,7 +2216,20 @@ send_items(Host, Node, NodeId, Type, LJID, Number) ->
[{xmlelement, "items", nodeAttr(Node),
itemsEls(ToSend)}])
end,
- ejabberd_router:route(service_jid(Host), jlib:make_jid(LJID), Stanza).
+ case is_tuple(Host) of
+ false ->
+ ejabberd_router:route(service_jid(Host), jlib:make_jid(LJID), Stanza);
+ true ->
+ case ejabberd_sm:get_session_pid(U,S,R) of
+ C2SPid when is_pid(C2SPid) ->
+ ejabberd_c2s:broadcast(C2SPid,
+ {pep_message, binary_to_list(Node)++"+notify"},
+ _Sender = service_jid(Host),
+ Stanza);
+ _ ->
+ ok
+ end
+ end.
%% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response}
%% Host = host()
@@ -2410,14 +2430,18 @@ get_options_helper(JID, Lang, Node, NodeID, SubID, Type) ->
read_sub(Subscriber, Node, NodeID, SubID, Lang) ->
case pubsub_subscription_odbc:get_subscription(Subscriber, NodeID, SubID) of
- {error, notfound} ->
- {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")};
{result, #pubsub_subscription{options = Options}} ->
{result, XdataEl} = pubsub_subscription_odbc:get_options_xform(Lang, Options),
OptionsEl = {xmlelement, "options", [{"jid", jlib:jid_to_string(Subscriber)},
{"subid", SubID}|nodeAttr(Node)],
[XdataEl]},
PubsubEl = {xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], [OptionsEl]},
+ {result, PubsubEl};
+ _ ->
+ OptionsEl = {xmlelement, "options", [{"jid", jlib:jid_to_string(Subscriber)},
+ {"subid", SubID}|nodeAttr(Node)],
+ []},
+ PubsubEl = {xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], [OptionsEl]},
{result, PubsubEl}
end.
@@ -2467,12 +2491,14 @@ set_options_helper(Configuration, JID, NodeID, SubID, Type) ->
write_sub(_Subscriber, _NodeID, _SubID, invalid) ->
{error, extended_error(?ERR_BAD_REQUEST, "invalid-options")};
+write_sub(_Subscriber, _NodeID, _SubID, []) ->
+ {result, []};
write_sub(Subscriber, NodeID, SubID, Options) ->
- case pubsub_subscription_odbc:set_subscription(Subscriber, NodeID, SubID, Options) of
- {error, notfound} ->
- {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")};
+ case pubsub_subscription:set_subscription(Subscriber, NodeID, SubID, Options) of
{result, _} ->
- {result, []}
+ {result, []};
+ {error, _} ->
+ {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}
end.
%% @spec (Host, Node, JID, Plugins) -> {error, Reason} | {result, Response}
@@ -2766,21 +2792,20 @@ sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) ->
sub_option_can_deliver(NotifyType, Depth, Option)
end, SubOptions).
+node_to_deliver(LJID, NodeOptions) ->
+ presence_can_deliver(LJID, get_option(NodeOptions, presence_based_delivery)).
+
sub_option_can_deliver(items, _, {subscription_type, nodes}) -> false;
sub_option_can_deliver(nodes, _, {subscription_type, items}) -> false;
sub_option_can_deliver(_, _, {subscription_depth, all}) -> true;
sub_option_can_deliver(_, Depth, {subscription_depth, D}) -> Depth =< D;
-sub_option_can_deliver(_, _, {deliver, false}) -> false;
-sub_option_can_deliver(_, _, {expire, When}) -> now() < When;
-sub_option_can_deliver(_, _, _) -> true.
-
-node_to_deliver(LJID, NodeOptions) ->
- PresenceDelivery = get_option(NodeOptions, presence_based_delivery),
- presence_can_deliver(LJID, PresenceDelivery).
+sub_option_can_deliver(_, _, {deliver, false}) -> false;
+sub_option_can_deliver(_, _, {expire, When}) -> now() < When;
+sub_option_can_deliver(_, _, _) -> true.
presence_can_deliver(_, false) -> true;
presence_can_deliver({User, Server, Resource}, true) ->
- case mnesia:dirty_match_object({session, '_', '_', {User, Server}, '_', '_'}) of
+ case ejabberd_sm:get_user_sessions(User, Server) of
[] -> false;
Ss ->
lists:foldl(fun(_, true) -> true;
@@ -2798,45 +2823,45 @@ state_can_deliver({U, S, R}, []) -> [{U, S, R}];
state_can_deliver({U, S, R}, SubOptions) ->
%% Check SubOptions for 'show_values'
case lists:keysearch('show_values', 1, SubOptions) of
- %% If not in suboptions, item can be delivered, case doesn't apply
- false -> [{U, S, R}];
- %% If in a suboptions ...
- {_, {_, ShowValues}} ->
- %% Get subscriber resources
- Resources = case R of
- %% If the subscriber JID is a bare one, get all its resources
- [] -> user_resources(U, S);
- %% If the subscriber JID is a full one, use its resource
- R -> [R]
- end,
- %% For each resource, test if the item is allowed to be delivered
- %% based on resource state
- lists:foldl(
- fun(Resource, Acc) ->
- get_resource_state({U, S, Resource}, ShowValues, Acc)
- end, [], Resources)
+ %% If not in suboptions, item can be delivered, case doesn't apply
+ false -> [{U, S, R}];
+ %% If in a suboptions ...
+ {_, {_, ShowValues}} ->
+ %% Get subscriber resources
+ Resources = case R of
+ %% If the subscriber JID is a bare one, get all its resources
+ [] -> user_resources(U, S);
+ %% If the subscriber JID is a full one, use its resource
+ R -> [R]
+ end,
+ %% For each resource, test if the item is allowed to be delivered
+ %% based on resource state
+ lists:foldl(
+ fun(Resource, Acc) ->
+ get_resource_state({U, S, Resource}, ShowValues, Acc)
+ end, [], Resources)
end.
get_resource_state({U, S, R}, ShowValues, JIDs) ->
%% Get user session PID
case ejabberd_sm:get_session_pid(U, S, R) of
- %% If no PID, item can be delivered
- none -> lists:append([{U, S, R}], JIDs);
- %% If PID ...
- Pid ->
- %% Get user resource state
- %% TODO : add a catch clause
- Show = case ejabberd_c2s:get_presence(Pid) of
- {_, _, "available", _} -> "online";
- {_, _, State, _} -> State
- end,
- %% Is current resource state listed in 'show-values' suboption ?
- case lists:member(Show, ShowValues) of %andalso Show =/= "online" of
- %% If yes, item can be delivered
- true -> lists:append([{U, S, R}], JIDs);
- %% If no, item can't be delivered
- false -> JIDs
- end
+ %% If no PID, item can be delivered
+ none -> lists:append([{U, S, R}], JIDs);
+ %% If PID ...
+ Pid ->
+ %% Get user resource state
+ %% TODO : add a catch clause
+ Show = case ejabberd_c2s:get_presence(Pid) of
+ {_, _, "available", _} -> "online";
+ {_, _, State, _} -> State
+ end,
+ %% Is current resource state listed in 'show-values' suboption ?
+ case lists:member(Show, ShowValues) of %andalso Show =/= "online" of
+ %% If yes, item can be delivered
+ true -> lists:append([{U, S, R}], JIDs);
+ %% If no, item can't be delivered
+ false -> JIDs
+ end
end.
%% @spec (Payload) -> int()
@@ -2862,224 +2887,150 @@ event_stanza_withmoreels(Els, MoreEls) ->
{xmlelement, "message", [],
[{xmlelement, "event", [{"xmlns", ?NS_PUBSUB_EVENT}], Els} | MoreEls]}.
+event_stanza(Event, EvAttr) ->
+ event_stanza([{xmlelement, Event, EvAttr, []}]).
+event_stanza(Event, EvAttr, Entries) ->
+ event_stanza([{xmlelement, Event, EvAttr,
+ [{xmlelement, Entry, EnAttr, []} || {Entry, EnAttr} <- Entries]}]).
+event_stanza(Event, EvAttr, Entry, EnAttr, Payload) ->
+ event_stanza([{xmlelement, Event, EvAttr, [{xmlelement, Entry, EnAttr, Payload}]}]).
+event_stanza(Event, EvAttr, Entry, EnAttr, Payload, Publisher) ->
+ Stanza = event_stanza(Event, EvAttr, Entry, EnAttr, Payload),
+ add_extended_headers(Stanza, extended_headers([jlib:jid_to_string(Publisher)])).
+
%%%%%% broadcast functions
-broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, Removed, ItemId, From, Payload) ->
- case get_collection_subscriptions(Host, Node) of
- SubsByDepth when is_list(SubsByDepth) ->
- Content = case get_option(NodeOptions, deliver_payloads) of
- true -> Payload;
- false -> []
- end,
- Stanza = event_stanza(
- [{xmlelement, "items", nodeAttr(Node),
- [{xmlelement, "item", itemAttr(ItemId), Content}]}]),
- broadcast_stanza(Host, From, Node, NodeId, Type,
- NodeOptions, SubsByDepth, items, Stanza, true),
- case Removed of
- [] ->
- ok;
- _ ->
- case get_option(NodeOptions, notify_retract) of
- true ->
- RetractStanza = event_stanza(
- [{xmlelement, "items", nodeAttr(Node),
- [{xmlelement, "retract", itemAttr(RId), []} || RId <- Removed]}]),
- broadcast_stanza(Host, Node, NodeId, Type,
- NodeOptions, SubsByDepth,
- items, RetractStanza, true);
- _ ->
- ok
- end
- end,
- {result, true};
+broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, ItemId, Publisher, Payload, Removed) ->
+ Publish = case get_option(NodeOptions, deliver_payloads) of
+ true -> event_stanza("items", nodeAttr(Node), "item", itemAttr(ItemId), Payload, Publisher);
+ false -> event_stanza("items", nodeAttr(Node), "item", itemAttr(ItemId), [], Publisher)
+ end,
+ case Removed of
+ [] ->
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, items, true, Publish, true)};
_ ->
- {result, false}
+ Retract = event_stanza("items", nodeAttr(Node), [{"retract", itemAttr(Rid)} || Rid <- Removed]),
+ Stanzas = [{true, Publish, true}, {get_option(NodeOptions, notify_retract), Retract, true}],
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, items, Stanzas)}
end.
broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds) ->
- broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, false).
-broadcast_retract_items(_Host, _Node, _NodeId, _Type, _NodeOptions, [], _ForceNotify) ->
+ broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, notify_retract).
+broadcast_retract_items(_Host, _Node, _NodeId, _Type, _NodeOptions, [], _) ->
{result, false};
-broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, ForceNotify) ->
- case (get_option(NodeOptions, notify_retract) or ForceNotify) of
- true ->
- case get_collection_subscriptions(Host, Node) of
- SubsByDepth when is_list(SubsByDepth) ->
- Stanza = event_stanza(
- [{xmlelement, "items", nodeAttr(Node),
- [{xmlelement, "retract", itemAttr(ItemId), []} || ItemId <- ItemIds]}]),
- broadcast_stanza(Host, Node, NodeId, Type,
- NodeOptions, SubsByDepth, items, Stanza, true),
- {result, true};
- _ ->
- {result, false}
- end;
- _ ->
- {result, false}
- end.
+broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, false) ->
+ broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, notify_retract);
+broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, Notify) ->
+ Stanza = event_stanza("items", nodeAttr(Node), [{"retract", itemAttr(Rid)} || Rid <- ItemIds]),
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, items, Notify, Stanza, true)}.
broadcast_purge_node(Host, Node, NodeId, Type, NodeOptions) ->
- case get_option(NodeOptions, notify_retract) of
- true ->
- case get_collection_subscriptions(Host, Node) of
- SubsByDepth when is_list(SubsByDepth) ->
- Stanza = event_stanza(
- [{xmlelement, "purge", nodeAttr(Node),
- []}]),
- broadcast_stanza(Host, Node, NodeId, Type,
- NodeOptions, SubsByDepth, nodes, Stanza, false),
- {result, true};
- _ ->
- {result, false}
- end;
- _ ->
- {result, false}
- end.
+ Stanza = event_stanza("purge", nodeAttr(Node)),
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, nodes, notify_retract, Stanza, false)}.
-broadcast_removed_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth) ->
- case get_option(NodeOptions, notify_delete) of
- true ->
- case SubsByDepth of
- [] ->
- {result, false};
- _ ->
- Stanza = event_stanza(
- [{xmlelement, "delete", nodeAttr(Node),
- []}]),
- broadcast_stanza(Host, Node, NodeId, Type,
- NodeOptions, SubsByDepth, nodes, Stanza, false),
- {result, true}
- end;
- _ ->
- {result, false}
- end.
+broadcast_removed_node(Host, Node, NodeId, Type, NodeOptions) ->
+ Stanza = event_stanza("delete", nodeAttr(Node)),
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, nodes, notify_delete, Stanza, false)}.
-broadcast_created_node(_, _, _, _, _, []) ->
- {result, false};
-broadcast_created_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth) ->
- Stanza = event_stanza([{xmlelement, "create", nodeAttr(Node), []}]),
- broadcast_stanza(Host, Node, NodeId, Type, NodeOptions, SubsByDepth, nodes, Stanza, true),
- {result, true}.
+broadcast_created_node(Host, Node, NodeId, Type, NodeOptions) ->
+ Stanza = event_stanza("create", nodeAttr(Node)),
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, nodes, true, Stanza, true)}.
broadcast_config_notification(Host, Node, NodeId, Type, NodeOptions, Lang) ->
- case get_option(NodeOptions, notify_config) of
+ Stanza = case get_option(NodeOptions, deliver_payloads) of
true ->
- case get_collection_subscriptions(Host, Node) of
- SubsByDepth when is_list(SubsByDepth) ->
- Content = case get_option(NodeOptions, deliver_payloads) of
- true ->
- [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}],
- get_configure_xfields(Type, NodeOptions, Lang, [])}];
- false ->
- []
+ event_stanza("configuration", nodeAttr(Node),
+ "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}],
+ get_configure_xfields(Type, NodeOptions, Lang, []));
+ false ->
+ event_stanza("configuration", nodeAttr(Node))
+ end,
+ {result, broadcast(Host, Node, NodeId, Type, NodeOptions, nodes, notify_config, Stanza, false)}.
+
+broadcast(Host, Node, NodeId, Type, NodeOptions, Notify, Stanzas) ->
+ Subs = node_subscriptions(Host, Node, NodeId, Type, NodeOptions, Notify),
+ Result = [broadcast(Host, Node, NodeId, Type, NodeOptions, Subs, Stanza, SHIM) ||
+ {Cond, Stanza, SHIM} <- Stanzas, Cond =:= true],
+ lists:member(true, Result).
+broadcast(Host, Node, NodeId, Type, NodeOptions, Notify, true, Stanza, SHIM) ->
+ Subs = node_subscriptions(Host, Node, NodeId, Type, NodeOptions, Notify),
+ broadcast(Host, Node, NodeId, Type, NodeOptions, Subs, Stanza, SHIM);
+broadcast(_Host, _Node, _NodeId, _Type, _NodeOptions, _Notify, false, _Stanza, _SHIM) ->
+ false;
+broadcast(Host, Node, NodeId, Type, NodeOptions, Notify, Condition, Stanza, SHIM) ->
+ broadcast(Host, Node, NodeId, Type, NodeOptions, Notify, get_option(NodeOptions, Condition), Stanza, SHIM).
+
+broadcast({U, S, R}, Node, NodeId, Type, NodeOptions, Subscriptions, Stanza, SHIM) ->
+ broadcast(S, Node, NodeId, Type, NodeOptions, Subscriptions, Stanza, SHIM)
+ or case ejabberd_sm:get_session_pid(U, S, user_resource(U, S, R)) of
+ C2SPid when is_pid(C2SPid) ->
+ %% 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
+ Event = {pep_message, binary_to_list(Node)++"+notify"},
+ Message = case get_option(NodeOptions, notification_type, headline) of
+ normal -> Stanza;
+ MsgType -> add_message_type(Stanza, atom_to_list(MsgType))
+ end,
+ ejabberd_c2s:broadcast(C2SPid, Event, jlib:make_jid(U, S, ""), Message),
+ true;
+ _ ->
+ ?DEBUG("~p@~p has no session; can't deliver stanza: ~p", [U, S, Stanza]),
+ false
+ end;
+broadcast(_Host, _Node, _NodeId, _Type, _NodeOptions, [], _Stanza, _SHIM) ->
+ false;
+broadcast(Host, _Node, _NodeId, _Type, NodeOptions, Subscriptions, Stanza, SHIM) ->
+ From = service_jid(Host),
+ Message = case get_option(NodeOptions, notification_type, headline) of
+ normal -> Stanza;
+ MsgType -> add_message_type(Stanza, atom_to_list(MsgType))
+ end,
+ lists:foreach(fun({LJID, NodeName, SubIds}) ->
+ Send = case {SHIM, SubIds} of
+ {false, _} -> Message;
+ {true, [_]} -> add_shim_headers(Message, collection_shim(NodeName));
+ {true, _} -> add_shim_headers(Message, lists:append(collection_shim(NodeName), subid_shim(SubIds)))
end,
- Stanza = event_stanza(
- [{xmlelement, "configuration", nodeAttr(Node), Content}]),
- broadcast_stanza(Host, Node, NodeId, Type,
- NodeOptions, SubsByDepth, nodes, Stanza, false),
- {result, true};
- _ ->
- {result, false}
- end;
+ ejabberd_router:route(From, jlib:make_jid(LJID), Send)
+ end, Subscriptions),
+ true.
+
+node_subscriptions(Host, Node, NodeId, Type, _NodeOptions, Notify) ->
+ % TODO temporary dirty condition, should be improved using plugin or node options
+ case Type of
+ "flat" -> node_subscriptions_bare(Host, Node, NodeId, Type);
+ "pep" -> node_subscriptions_bare(Host, Node, NodeId, Type);
+ _ -> node_subscriptions_full(Host, Node, Notify)
+ end.
+
+node_subscriptions_bare(Host, Node, NodeId, Type) ->
+ case node_action(Host, Type, get_node_subscriptions, [NodeId]) of
+ {result, Subs} ->
+ SubsByJid = lists:foldl(
+ fun({JID, subscribed, SubId}, Acc) ->
+ case dict:is_key(JID, Acc) of
+ true -> dict:append(JID, SubId, Acc);
+ false -> dict:store(JID, [SubId], Acc)
+ end;
+ (_, Acc) ->
+ Acc
+ end, dict:new(), Subs),
+ [{J, Node, S} || {J, S} <- dict:to_list(SubsByJid)];
_ ->
- {result, false}
+ []
end.
-get_collection_subscriptions(Host, Node) ->
+node_subscriptions_full(Host, Node, NotifyType) ->
Action = fun() ->
- {result, lists:map(fun({Depth, Nodes}) ->
- {Depth, [{N, get_node_subs(N)} || N <- Nodes]}
- end, tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]))}
+ Collection = tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]),
+ {result, [{Depth, [{N, sub_with_options(N)} || N <- Nodes]} || {Depth, Nodes} <- Collection]}
end,
case transaction(Host, Action, sync_dirty) of
- {result, CollSubs} -> CollSubs;
+ {result, CollSubs} -> subscribed_nodes_by_jid(NotifyType, CollSubs);
_ -> []
end.
-get_node_subs(#pubsub_node{type = Type,
- id = NodeID}) ->
- case node_call(Type, get_node_subscriptions, [NodeID]) of
- {result, Subs} -> get_options_for_subs(NodeID, Subs);
- Other -> Other
- end.
-
-get_options_for_subs(NodeID, Subs) ->
- lists:foldl(fun({JID, subscribed, SubID}, Acc) ->
- case pubsub_subscription_odbc:get_subscription(JID, NodeID, SubID) of
- {error, notfound} -> [{JID, SubID, []} | Acc];
- {result, #pubsub_subscription{options = Options}} -> [{JID, SubID, Options} | Acc];
- _ -> Acc
- end;
- (_, Acc) ->
- Acc
- end, [], Subs).
-
-broadcast_stanza(Host, _Node, _NodeId, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) ->
- 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, atom_to_list(MsgType))
- end,
- %% Handles explicit subscriptions
- SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth),
- lists:foreach(fun ({LJID, NodeName, SubIDs}) ->
- LJIDs = case BroadcastAll of
- true ->
- {U, S, _} = LJID,
- [{U, S, R} || R <- user_resources(U, S)];
- false ->
- [LJID]
- end,
- %% Determine if the stanza should have SHIM ('SubID' and 'name') headers
- StanzaToSend = case {SHIM, SubIDs} of
- {false, _} ->
- Stanza;
- %% If there's only one SubID, don't add it
- {true, [_]} ->
- add_shim_headers(Stanza, collection_shim(NodeName));
- {true, SubIDs} ->
- add_shim_headers(Stanza, lists:append(collection_shim(NodeName), subid_shim(SubIDs)))
- end,
- lists:foreach(fun(To) ->
- ejabberd_router:route(From, jlib:make_jid(To), StanzaToSend)
- end, LJIDs)
- end, SubIDsByJID).
-
-broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) ->
- broadcast_stanza({LUser, LServer, LResource}, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM),
- %% Handles implicit presence subscriptions
- SenderResource = case LResource of
- [] ->
- case user_resources(LUser, LServer) of
- [Resource|_] -> Resource;
- _ -> ""
- end;
- _ ->
- LResource
- end,
- 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, atom_to_list(MsgType))
- end,
- %% 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
- ejabberd_c2s:broadcast(C2SPid,
- {pep_message, binary_to_list(Node)++"+notify"},
- _Sender = jlib:make_jid(LUser, LServer, ""),
- _StanzaToSend = add_extended_headers(Stanza,
- _ReplyTo = extended_headers([jlib:jid_to_string(Publisher)])));
- _ ->
- ?DEBUG("~p@~p has no session; can't deliver ~p to contacts", [LUser, LServer, BaseStanza])
- end;
-broadcast_stanza(Host, _Publisher, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) ->
- broadcast_stanza(Host, Node, NodeId, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM).
-
subscribed_nodes_by_jid(NotifyType, SubsByDepth) ->
NodesToDeliver = fun(Depth, Node, Subs, Acc) ->
NodeName = case Node#pubsub_node.nodeid of
@@ -3089,7 +3040,7 @@ subscribed_nodes_by_jid(NotifyType, SubsByDepth) ->
NodeOptions = Node#pubsub_node.options,
lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) ->
case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of
- true ->
+ true ->
%% If is to deliver :
case state_can_deliver(LJID, SubOptions) of
[] -> {JIDs, Recipients};
@@ -3098,13 +3049,13 @@ subscribed_nodes_by_jid(NotifyType, SubsByDepth) ->
fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) ->
case lists:member(JIDToDeliver, JIDs) of
%% check if the JIDs co-accumulator contains the Subscription Jid,
- false ->
+ false ->
%% - if not,
%% - add the Jid to JIDs list co-accumulator ;
%% - create a tuple of the Jid, NodeId, and SubID (as list),
%% and add the tuple to the Recipients list co-accumulator
{[JIDToDeliver | JIDsAcc], [{JIDToDeliver, NodeName, [SubID]} | RecipientsAcc]};
- true ->
+ true ->
%% - if the JIDs co-accumulator contains the Jid
%% get the tuple containing the Jid from the Recipient list co-accumulator
{_, {JIDToDeliver, NodeName1, SubIDs}} = lists:keysearch(JIDToDeliver, 1, RecipientsAcc),
@@ -3133,9 +3084,33 @@ subscribed_nodes_by_jid(NotifyType, SubsByDepth) ->
{_, JIDSubs} = lists:foldl(DepthsToDeliver, {[], []}, SubsByDepth),
JIDSubs.
+sub_with_options(#pubsub_node{type = Type, id = NodeId}) ->
+ case node_call(Type, get_node_subscriptions, [NodeId]) of
+ {result, Subs} ->
+ lists:foldl(
+ fun({JID, subscribed, SubId}, Acc) -> [sub_with_options(JID, NodeId, SubId) | Acc];
+ (_, Acc) -> Acc
+ end, [], Subs);
+ _ ->
+ []
+ end.
+sub_with_options(JID, NodeId, SubId) ->
+ case pubsub_subscription_odbc:get_subscription(JID, NodeId, SubId) of
+ {result, #pubsub_subscription{options = Options}} -> {JID, SubId, Options};
+ _ -> {JID, SubId, []}
+ end.
+
user_resources(User, Server) ->
ejabberd_sm:get_user_resources(User, Server).
+user_resource(User, Server, []) ->
+ case user_resources(User, Server) of
+ [R|_] -> R;
+ _ -> []
+ end;
+user_resource(_, _, Resource) ->
+ Resource.
+
%%%%%%% Configuration handling
%%<p>There are several reasons why the default node configuration options request might fail:</p>
@@ -3794,7 +3769,7 @@ extended_headers(Jids) ->
on_user_offline(_, JID, _) ->
{User, Server, Resource} = jlib:jid_tolower(JID),
- case ejabberd_sm:get_user_resources(User, Server) of
+ case user_resources(User, Server) of
[] -> purge_offline({User, Server, Resource});
_ -> true
end.
diff --git a/src/mod_pubsub/node_flat_odbc.erl b/src/mod_pubsub/node_flat_odbc.erl
index dc0dca0f9..b35381043 100644
--- a/src/mod_pubsub/node_flat_odbc.erl
+++ b/src/mod_pubsub/node_flat_odbc.erl
@@ -76,8 +76,7 @@ terminate(Host, ServerHost) ->
node_hometree_odbc:terminate(Host, ServerHost).
options() ->
- [{node_type, flat},
- {deliver_payloads, true},
+ [{deliver_payloads, true},
{notify_config, false},
{notify_delete, false},
{notify_retract, true},
diff --git a/src/mod_pubsub/node_hometree.erl b/src/mod_pubsub/node_hometree.erl
index 8c32caaa9..ec3436e48 100644
--- a/src/mod_pubsub/node_hometree.erl
+++ b/src/mod_pubsub/node_hometree.erl
@@ -97,9 +97,11 @@ init(_Host, _ServerHost, _Options) ->
pubsub_subscription:init(),
mnesia:create_table(pubsub_state,
[{disc_copies, [node()]},
+ {index, [nodeidx]},
{attributes, record_info(fields, pubsub_state)}]),
mnesia:create_table(pubsub_item,
[{disc_only_copies, [node()]},
+ {index, [nodeidx]},
{attributes, record_info(fields, pubsub_item)}]),
ItemsFields = record_info(fields, pubsub_item),
case mnesia:table_info(pubsub_item, attributes) of
@@ -224,7 +226,7 @@ create_node_permission(Host, ServerHost, NodeId, _ParentNodeId, Owner, Access) -
%% @doc <p></p>
create_node(NodeIdx, Owner) ->
OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)),
- set_state(#pubsub_state{stateid = {OwnerKey, NodeIdx}, affiliation = owner}),
+ set_state(#pubsub_state{stateid = {OwnerKey, NodeIdx}, nodeidx = NodeIdx, affiliation = owner}),
{result, {default, broadcast}}.
%% @spec (Nodes) -> {result, {default, broadcast, Reply}}
@@ -507,6 +509,7 @@ publish_item(NodeIdx, Publisher, PublishModel, MaxItems, ItemId, Payload) ->
payload = Payload};
_ ->
#pubsub_item{itemid = {ItemId, NodeIdx},
+ nodeidx = NodeIdx,
creation = {Now, GenKey},
modification = PubId,
payload = Payload}
@@ -860,8 +863,7 @@ get_nodes_helper(NodeTree,
%% ```get_states(NodeIdx) ->
%% node_default:get_states(NodeIdx).'''</p>
get_states(NodeIdx) ->
- States = case catch mnesia:match_object(
- #pubsub_state{stateid = {'_', NodeIdx}, _ = '_'}) of
+ States = case catch mnesia:index_read(pubsub_state, NodeIdx, #pubsub_state.nodeidx) of
List when is_list(List) -> List;
_ -> []
end,
@@ -876,7 +878,7 @@ get_state(NodeIdx, JID) ->
StateId = {JID, NodeIdx},
case catch mnesia:read({pubsub_state, StateId}) of
[State] when is_record(State, pubsub_state) -> State;
- _ -> #pubsub_state{stateid=StateId}
+ _ -> #pubsub_state{stateid=StateId, nodeidx=NodeIdx}
end.
%% @spec (State) -> ok | {error, Reason}
@@ -911,7 +913,7 @@ del_state(NodeIdx, JID) ->
%% ```get_items(NodeIdx, From) ->
%% node_default:get_items(NodeIdx, From).'''</p>
get_items(NodeIdx, _From) ->
- Items = mnesia:match_object(#pubsub_item{itemid = {'_', NodeIdx}, _ = '_'}),
+ Items = mnesia:index_read(pubsub_item, NodeIdx, #pubsub_item.nodeidx),
{result, lists:reverse(lists:keysort(#pubsub_item.modification, Items))}.
get_items(NodeIdx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) ->
diff --git a/src/mod_pubsub/node_pep_odbc.erl b/src/mod_pubsub/node_pep_odbc.erl
index 125ba1331..387327c8e 100644
--- a/src/mod_pubsub/node_pep_odbc.erl
+++ b/src/mod_pubsub/node_pep_odbc.erl
@@ -86,7 +86,6 @@ terminate(Host, ServerHost) ->
options() ->
[{odbc, true},
- {node_type, pep},
{deliver_payloads, true},
{notify_config, false},
{notify_delete, false},
diff --git a/src/mod_pubsub/pubsub.hrl b/src/mod_pubsub/pubsub.hrl
index 41c4e6a47..056c7a8b7 100644
--- a/src/mod_pubsub/pubsub.hrl
+++ b/src/mod_pubsub/pubsub.hrl
@@ -146,6 +146,7 @@
-record(pubsub_state,
{
stateid,
+ nodeidx,
items = [],
affiliation = 'none',
subscriptions = []
@@ -161,6 +162,7 @@
-record(pubsub_item,
{
itemid,
+ nodeidx,
creation = {'unknown','unknown'},
modification = {'unknown','unknown'},
payload = []
diff --git a/src/mod_pubsub/pubsub_clean.erl b/src/mod_pubsub/pubsub_clean.erl
new file mode 100644
index 000000000..4b59e0821
--- /dev/null
+++ b/src/mod_pubsub/pubsub_clean.erl
@@ -0,0 +1,42 @@
+-module(pubsub_clean).
+
+-define(TIMEOUT, 1000*600). % 1 minute
+
+-export([start/0, loop/0, purge/0, offline/1]).
+
+start() ->
+ Pid = spawn(?MODULE, loop, []),
+ register(pubsub_clean, Pid),
+ Pid.
+
+loop() ->
+ receive
+ purge -> purge()
+ after ?TIMEOUT -> purge()
+ end,
+ loop().
+
+purge() ->
+ Sessions = lists:sum([mnesia:table_info(session,size)|[rpc:call(N,mnesia,table_info,[session,size]) || N <- nodes()]]),
+ Subscriptions = mnesia:table_info(pubsub_state,size),
+ if Subscriptions > Sessions + 500 ->
+ lists:foreach(fun(K) ->
+ [N]=mnesia:dirty_read({pubsub_node, K}),
+ I=element(3,N),
+ lists:foreach(fun(JID) ->
+ case mnesia:dirty_read({pubsub_state, {JID, I}}) of
+ [{pubsub_state, K, _, _, _, [{subscribed,S}]}] -> mnesia:dirty_delete({pubsub_subscription, S});
+ _ -> ok
+ end,
+ mnesia:dirty_delete({pubsub_state, {JID, I}})
+ end, offline(pubsub_debug:subscribed(I)))
+ end, mnesia:dirty_all_keys(pubsub_node));
+ true ->
+ ok
+ end.
+
+offline(Jids) ->
+ lists:filter(fun({U,S,""}) -> ejabberd_sm:get_user_resources(U,S) == [];
+ ({U,S,R}) -> not lists:member(R,ejabberd_sm:get_user_resources(U,S))
+ end, Jids).
+%%ejabberd_cluster:get_node({LUser, LServer})
diff --git a/src/mod_pubsub/pubsub_debug.erl b/src/mod_pubsub/pubsub_debug.erl
new file mode 100644
index 000000000..791031e5e
--- /dev/null
+++ b/src/mod_pubsub/pubsub_debug.erl
@@ -0,0 +1,113 @@
+-module(pubsub_debug).
+-author('christophe.romain@process-one.net').
+
+-include("pubsub.hrl").
+
+-compile(export_all).
+
+nodeid(Host, Node) ->
+ case mnesia:dirty_read({pubsub_node, {Host, Node}}) of
+ [N] -> nodeid(N);
+ _ -> 0
+ end.
+nodeid(N) -> N#pubsub_node.id.
+nodeids() -> [nodeid(Host, Node) || {Host, Node} <- mnesia:dirty_all_keys(pubsub_node)].
+nodeids_by_type(Type) -> [nodeid(N) || N <- mnesia:dirty_match_object(#pubsub_node{type=Type, _='_'})].
+nodeids_by_option(Key, Value) -> [nodeid(N) || N <- mnesia:dirty_match_object(#pubsub_node{_='_'}), lists:member({Key, Value}, N#pubsub_node.options)].
+nodeids_by_owner(JID) -> [nodeid(N) || N <- mnesia:dirty_match_object(#pubsub_node{_='_'}), lists:member(JID, N#pubsub_node.owners)].
+nodes_by_id(I) -> mnesia:dirty_match_object(#pubsub_node{id=I, _='_'}).
+nodes() -> [element(2, element(2, N)) || N <- mnesia:dirty_match_object(#pubsub_node{_='_'})].
+
+state(JID, NodeId) ->
+ case mnesia:dirty_read({pubsub_state, {JID, NodeId}}) of
+ [S] -> S;
+ _ -> undefined
+ end.
+states(NodeId) -> mnesia:dirty_index_read(pubsub_state, NodeId, #pubsub_state.nodeidx).
+stateid(S) -> element(1, S#pubsub_state.stateid).
+stateids(NodeId) -> [stateid(S) || S <- states(NodeId)].
+states_by_jid(JID) -> mnesia:dirty_match_object(#pubsub_state{stateid={JID, '_'}, _='_'}).
+
+item(ItemId, NodeId) ->
+ case mnesia:dirty_read({pubsub_item, {ItemId, NodeId}}) of
+ [I] -> I;
+ _ -> undefined
+ end.
+items(NodeId) -> mnesia:dirty_index_read(pubsub_item, NodeId, #pubsub_item.nodeidx).
+itemid(I) -> element(1, I#pubsub_item.itemid).
+itemids(NodeId) -> [itemid(I) || I <- items(NodeId)].
+items_by_id(ItemId) -> mnesia:dirty_match_object(#pubsub_item{itemid={ItemId, '_'}, _='_'}).
+
+affiliated(NodeId) -> [stateid(S) || S <- states(NodeId), S#pubsub_state.affiliation=/=none].
+subscribed(NodeId) -> [stateid(S) || S <- states(NodeId), S#pubsub_state.subscriptions=/=[]].
+%subscribed(NodeId) -> [stateid(S) || S <- states(NodeId), S#pubsub_state.subscription=/=none]. %% old record
+owners(NodeId) -> [stateid(S) || S <- states(NodeId), S#pubsub_state.affiliation==owner].
+
+orphan_items(NodeId) ->
+ itemids(NodeId) -- lists:foldl(fun(S, A) -> A++S#pubsub_state.items end, [], states(NodeId)).
+newer_items(NodeId, Seconds) ->
+ Now = calendar:universal_time(),
+ Oldest = calendar:seconds_to_daystime(Seconds),
+ [itemid(I) || I <- items(NodeId), calendar:time_difference(calendar:now_to_universal_time(element(1, I#pubsub_item.modification)), Now) < Oldest].
+older_items(NodeId, Seconds) ->
+ Now = calendar:universal_time(),
+ Oldest = calendar:seconds_to_daystime(Seconds),
+ [itemid(I) || I <- items(NodeId), calendar:time_difference(calendar:now_to_universal_time(element(1, I#pubsub_item.modification)), Now) > Oldest].
+
+orphan_nodes() -> [I || I <- nodeids(), owners(I)==[]].
+duplicated_nodes() -> L = nodeids(), lists:usort(L -- lists:seq(1, lists:max(L))).
+node_options(NodeId) ->
+ [N] = mnesia:dirty_match_object(#pubsub_node{id=NodeId, _='_'}),
+ N#pubsub_node.options.
+update_node_options(Key, Value, NodeId) ->
+ [N] = mnesia:dirty_match_object(#pubsub_node{id=NodeId, _='_'}),
+ NewOptions = lists:keyreplace(Key, 1, N#pubsub_node.options, {Key, Value}),
+ mnesia:dirty_write(N#pubsub_node{options = NewOptions}).
+
+check() ->
+ mnesia:transaction(fun() ->
+ case mnesia:read({pubsub_index, node}) of
+ [Idx] ->
+ Free = Idx#pubsub_index.free,
+ Last = Idx#pubsub_index.last,
+ Allocated = lists:seq(1, Last) -- Free,
+ NodeIds = mnesia:foldl(fun(N,A) -> [nodeid(N)|A] end, [], pubsub_node),
+ StateIds = lists:usort(mnesia:foldl(fun(S,A) -> [element(2, S#pubsub_state.stateid)|A] end, [], pubsub_state)),
+ ItemIds = lists:usort(mnesia:foldl(fun(I,A) -> [element(2, I#pubsub_item.itemid)|A] end, [], pubsub_item)),
+ BadNodeIds = NodeIds -- Allocated,
+ BadStateIds = StateIds -- NodeIds,
+ BadItemIds = ItemIds -- NodeIds,
+ Lost = Allocated -- NodeIds,
+ [{bad_nodes, [N#pubsub_node.nodeid || N <- lists:flatten([mnesia:match_object(#pubsub_node{id=I, _='_'}) || I <- BadNodeIds])]},
+ {bad_states, lists:foldl(fun(N,A) -> A++[{I,N} || I <- stateids(N)] end, [], BadStateIds)},
+ {bad_items, lists:foldl(fun(N,A) -> A++[{I,N} || I <- itemids(N)] end, [], BadItemIds)},
+ {lost_idx, Lost},
+ {orphaned, [I || I <- NodeIds, owners(I)==[]]},
+ {duplicated, lists:usort(NodeIds -- lists:seq(1, lists:max(NodeIds)))}];
+ _ ->
+ no_index
+ end
+ end).
+
+rebuild_index() ->
+ mnesia:transaction(fun() ->
+ NodeIds = mnesia:foldl(fun(N,A) -> [nodeid(N)|A] end, [], pubsub_node),
+ Last = lists:max(NodeIds),
+ Free = lists:seq(1, Last) -- NodeIds,
+ mnesia:write(#pubsub_index{index = node, last = Last, free = Free})
+ end).
+
+pep_subscriptions(LUser, LServer, LResource) ->
+ case ejabberd_sm:get_session_pid({LUser, LServer, LResource}) of
+ C2SPid when is_pid(C2SPid) ->
+ case catch ejabberd_c2s:get_subscribed(C2SPid) of
+ Contacts when is_list(Contacts) ->
+ lists:map(fun({U, S, _}) ->
+ io_lib:format("~s@~s", [U, S])
+ end, Contacts);
+ _ ->
+ []
+ end;
+ _ ->
+ []
+ end.
diff --git a/src/mod_pubsub/pubsub_odbc.patch b/src/mod_pubsub/pubsub_odbc.patch
index b7c18bacc..bfb26bfc1 100644
--- a/src/mod_pubsub/pubsub_odbc.patch
+++ b/src/mod_pubsub/pubsub_odbc.patch
@@ -1,5 +1,5 @@
---- mod_pubsub.erl 2012-04-11 16:47:33.620900390 +0200
-+++ mod_pubsub_odbc.erl 2012-04-11 16:47:53.390899087 +0200
+--- mod_pubsub.erl 2012-04-26 16:29:53.653392761 +0200
++++ mod_pubsub_odbc.erl 2012-04-26 16:29:53.616726238 +0200
@@ -42,7 +42,7 @@
%%% 6.2.3.1, 6.2.3.5, and 6.3. For information on subscription leases see
%%% XEP-0060 section 12.18.
@@ -49,7 +49,7 @@
put(server_host, ServerHost), % not clean, but needed to plug hooks at any location
init_nodes(Host, ServerHost, NodeTree, Plugins),
State = #state{host = Host,
-@@ -283,207 +281,14 @@
+@@ -283,256 +281,14 @@
init_nodes(Host, ServerHost, _NodeTree, Plugins) ->
%% TODO, this call should be done plugin side
@@ -253,6 +253,55 @@
- ?ERROR_MSG("Problem updating Pubsub state tables:~n~p",
- [Reason])
- end;
+- [stateid, items, affiliation, subscriptions] ->
+- ?INFO_MSG("upgrade state pubsub table", []),
+- F = fun ({pubsub_state, {JID, Nidx}, Items, Aff, Subs}, Acc) ->
+- NewState = #pubsub_state{stateid = {JID, Nidx},
+- nodeidx = Nidx,
+- items = Items,
+- affiliation = Aff,
+- subscriptions = Subs},
+- [NewState | Acc]
+- end,
+- {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3,
+- [F, [], pubsub_state]),
+- {atomic, ok} = mnesia:delete_table(pubsub_state),
+- {atomic, ok} = mnesia:create_table(pubsub_state,
+- [{disc_copies, [node()]},
+- {attributes, record_info(fields, pubsub_state)}]),
+- FNew = fun () ->
+- lists:foreach(fun mnesia:write/1, NewRecs)
+- end,
+- case mnesia:transaction(FNew) of
+- {atomic, Res1} ->
+- ?INFO_MSG("Pubsub state tables updated correctly: ~p", [Res1]);
+- {aborted, Rea1} ->
+- ?ERROR_MSG("Problem updating Pubsub state table:~n~p", [Rea1])
+- end,
+- ?INFO_MSG("upgrade item pubsub table", []),
+- F = fun ({pubsub_item, {ItemId, Nidx}, C, M, P}, Acc) ->
+- NewItem = #pubsub_item{itemid = {ItemId, Nidx},
+- nodeidx = Nidx,
+- creation = C,
+- modification = M,
+- payload = P},
+- [NewItem | Acc]
+- end,
+- {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3,
+- [F, [], pubsub_item]),
+- {atomic, ok} = mnesia:delete_table(pubsub_item),
+- {atomic, ok} = mnesia:create_table(pubsub_item,
+- [{disc_copies, [node()]},
+- {attributes, record_info(fields, pubsub_item)}]),
+- FNew = fun () ->
+- lists:foreach(fun mnesia:write/1, NewRecs)
+- end,
+- case mnesia:transaction(FNew) of
+- {atomic, Res2} ->
+- ?INFO_MSG("Pubsub item tables updated correctly: ~p", [Res2]);
+- {aborted, Rea2} ->
+- ?ERROR_MSG("Problem updating Pubsub item table:~n~p", [Rea2])
+- end;
- _ ->
- ok
- end.
@@ -260,7 +309,7 @@
send_loop(State) ->
receive
{presence, JID, Pid} ->
-@@ -494,17 +299,15 @@
+@@ -543,17 +299,15 @@
%% for each node From is subscribed to
%% and if the node is so configured, send the last published item to From
lists:foreach(fun(PType) ->
@@ -284,7 +333,7 @@
true ->
% resource not concerned about that subscription
ok
-@@ -623,7 +426,8 @@
+@@ -672,7 +426,8 @@
disco_identity(_Host, <<>>, _From) ->
[{xmlelement, "identity", [{"category", "pubsub"}, {"type", "pep"}], []}];
disco_identity(Host, Node, From) ->
@@ -294,7 +343,7 @@
case get_allowed_items_call(Host, Idx, From, Type, Options, Owners) of
{result, _} ->
{result, [{xmlelement, "identity", [{"category", "pubsub"}, {"type", "pep"}], []},
-@@ -658,7 +462,8 @@
+@@ -707,7 +462,8 @@
[?NS_PUBSUB
| [?NS_PUBSUB++"#"++Feature || Feature <- features("pep")]];
disco_features(Host, Node, From) ->
@@ -304,7 +353,7 @@
case get_allowed_items_call(Host, Idx, From, Type, Options, Owners) of
{result, _} ->
{result, [?NS_PUBSUB
-@@ -683,7 +488,8 @@
+@@ -732,7 +488,8 @@
Acc.
disco_items(Host, <<>>, From) ->
@@ -314,7 +363,7 @@
case get_allowed_items_call(Host, Idx, From, Type, Options, Owners) of
{result, _} ->
[{xmlelement, "item",
-@@ -701,13 +507,14 @@
+@@ -750,13 +507,14 @@
_ -> Acc
end
end,
@@ -331,7 +380,7 @@
case get_allowed_items_call(Host, Idx, From, Type, Options, Owners) of
{result, Items} ->
{result, [{xmlelement, "item",
-@@ -793,10 +600,10 @@
+@@ -842,10 +600,10 @@
lists:foreach(fun(PType) ->
{result, Subscriptions} = node_action(Host, PType, get_entity_subscriptions, [Host, Entity]),
lists:foreach(fun
@@ -344,7 +393,16 @@
true ->
node_action(Host, PType, unsubscribe_node, [NodeId, Entity, JID, all]);
false ->
-@@ -964,7 +771,8 @@
+@@ -879,7 +637,7 @@
+ {result, Affiliations} = node_action(Host, PType, get_entity_affiliations, [Host, Entity]),
+ lists:foreach(fun
+ ({#pubsub_node{nodeid = {H, N}, parents = []}, owner}) -> delete_node(H, N, Entity);
+- ({#pubsub_node{nodeid = {H, N}, type = "hometree"}, owner}) when N == HomeTreeBase -> delete_node(H, N, Entity);
++ ({#pubsub_node{nodeid = {H, N}, type = "hometree_odbc"}, owner}) when N == HomeTreeBase -> delete_node(H, N, Entity);
+ ({#pubsub_node{id = NodeId}, publisher}) -> node_action(Host, PType, set_affiliation, [NodeId, Entity, none]);
+ (_) -> ok
+ end, Affiliations)
+@@ -1013,7 +771,8 @@
sub_el = SubEl} = IQ ->
{xmlelement, _, QAttrs, _} = SubEl,
Node = xml:get_attr_s("node", QAttrs),
@@ -354,7 +412,7 @@
{result, IQRes} ->
jlib:iq_to_xml(
IQ#iq{type = result,
-@@ -1077,7 +885,7 @@
+@@ -1126,7 +885,7 @@
[] ->
["leaf"]; %% No sub-nodes: it's a leaf node
_ ->
@@ -363,7 +421,7 @@
{result, []} -> ["collection"];
{result, _} -> ["leaf", "collection"];
_ -> []
-@@ -1093,8 +901,9 @@
+@@ -1142,8 +901,9 @@
[];
true ->
[{xmlelement, "feature", [{"var", ?NS_PUBSUB}], []} |
@@ -375,7 +433,7 @@
end, features(Type))]
end,
%% TODO: add meta-data info (spec section 5.4)
-@@ -1123,8 +932,9 @@
+@@ -1172,8 +932,9 @@
{xmlelement, "feature", [{"var", ?NS_PUBSUB}], []},
{xmlelement, "feature", [{"var", ?NS_COMMANDS}], []},
{xmlelement, "feature", [{"var", ?NS_VCARD}], []}] ++
@@ -387,7 +445,7 @@
end, features(Host, Node))};
<<?NS_COMMANDS>> ->
command_disco_info(Host, Node, From);
-@@ -1134,7 +944,7 @@
+@@ -1183,7 +944,7 @@
node_disco_info(Host, Node, From)
end.
@@ -396,7 +454,7 @@
case tree_action(Host, get_subnodes, [Host, <<>>, From]) of
Nodes when is_list(Nodes) ->
{result, lists:map(
-@@ -1151,23 +961,24 @@
+@@ -1200,23 +961,24 @@
Other ->
Other
end;
@@ -427,7 +485,7 @@
end,
Nodes = lists:map(
fun(#pubsub_node{nodeid = {_, SubNode}, options = SubOptions}) ->
-@@ -1185,7 +996,7 @@
+@@ -1234,7 +996,7 @@
{result, Name} = node_call(Type, get_item_name, [Host, Node, RN]),
{xmlelement, "item", [{"jid", Host}, {"name", Name}], []}
end, NodeItems),
@@ -436,7 +494,7 @@
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Result}} -> {result, Result};
-@@ -1296,7 +1107,8 @@
+@@ -1345,7 +1107,8 @@
(_, Acc) ->
Acc
end, [], xml:remove_cdata(Els)),
@@ -446,7 +504,7 @@
{get, "subscriptions"} ->
get_subscriptions(Host, Node, From, Plugins);
{get, "affiliations"} ->
-@@ -1319,7 +1131,9 @@
+@@ -1368,7 +1131,9 @@
iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) ->
{xmlelement, _, _, SubEls} = SubEl,
@@ -457,7 +515,7 @@
case Action of
[{xmlelement, Name, Attrs, Els}] ->
Node = string_to_node(xml:get_attr_s("node", Attrs)),
-@@ -1449,7 +1263,8 @@
+@@ -1498,7 +1263,8 @@
_ -> []
end
end,
@@ -467,7 +525,7 @@
sync_dirty) of
{result, Res} -> Res;
Err -> Err
-@@ -1488,7 +1303,7 @@
+@@ -1537,7 +1303,7 @@
%%% authorization handling
@@ -476,7 +534,7 @@
Lang = "en", %% TODO fix
Stanza = {xmlelement, "message",
[],
-@@ -1517,7 +1332,7 @@
+@@ -1566,7 +1332,7 @@
[{xmlelement, "value", [], [{xmlcdata, "false"}]}]}]}]},
lists:foreach(fun(Owner) ->
ejabberd_router:route(service_jid(Host), jlib:make_jid(Owner), Stanza)
@@ -485,7 +543,7 @@
find_authorization_response(Packet) ->
{xmlelement, _Name, _Attrs, Els} = Packet,
-@@ -1581,8 +1396,8 @@
+@@ -1630,8 +1396,8 @@
"true" -> true;
_ -> false
end,
@@ -496,16 +554,16 @@
{result, Subscriptions} = node_call(Type, get_subscriptions, [NodeId, Subscriber]),
if
not IsApprover ->
-@@ -1781,7 +1596,7 @@
+@@ -1828,7 +1594,7 @@
Reply = [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}],
[{xmlelement, "create", nodeAttr(Node),
[]}]}],
- case transaction(CreateNode, transaction) of
+ case transaction(Host, CreateNode, transaction) of
- {result, {NodeId, SubsByDepth, {Result, broadcast}}} ->
- broadcast_created_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth),
+ {result, {NodeId, {Result, broadcast}}} ->
+ broadcast_created_node(Host, Node, NodeId, Type, NodeOptions),
ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, NodeId, NodeOptions]),
-@@ -1898,7 +1713,7 @@
+@@ -1943,7 +1709,7 @@
%%<li>The node does not exist.</li>
%%</ul>
subscribe_node(Host, Node, From, JID, Configuration) ->
@@ -514,7 +572,7 @@
{result, GoodSubOpts} -> GoodSubOpts;
_ -> invalid
end,
-@@ -1906,7 +1721,7 @@
+@@ -1951,7 +1717,7 @@
error -> {"", "", ""};
J -> jlib:jid_tolower(J)
end,
@@ -523,7 +581,7 @@
Features = features(Type),
SubscribeFeature = lists:member("subscribe", Features),
OptionsFeature = lists:member("subscription-options", Features),
-@@ -1915,6 +1730,7 @@
+@@ -1960,6 +1726,7 @@
AccessModel = get_option(Options, access_model),
SendLast = get_option(Options, send_last_published_item),
AllowedGroups = get_option(Options, roster_groups_allowed, []),
@@ -531,30 +589,7 @@
{PresenceSubscription, RosterGroup} = get_presence_and_roster_permissions(Host, Subscriber, Owners, AccessModel, AllowedGroups),
if
not SubscribeFeature ->
-@@ -2036,12 +1852,9 @@
- Features = features(Type),
- PublishFeature = lists:member("publish", Features),
- PublishModel = get_option(Options, publish_model),
-+ MaxItems = max_items(Host, Options),
- DeliverPayloads = get_option(Options, deliver_payloads),
- PersistItems = get_option(Options, persist_items),
-- MaxItems = case PersistItems of
-- false -> 0;
-- true -> max_items(Host, Options)
-- end,
- PayloadCount = payload_xmlelements(Payload),
- PayloadSize = size(term_to_binary(Payload))-2, % size(term_to_binary([])) == 2
- PayloadMaxSize = get_option(Options, max_payload_size),
-@@ -2092,7 +1905,7 @@
- false ->
- ok
- end,
-- set_cached_item(Host, NodeId, ItemId, Publisher, Payload),
-+ set_cached_item(Host, NodeId, ItemId, Publisher, Payload),
- case Result of
- default -> {result, Reply};
- _ -> {result, Result}
-@@ -2258,7 +2071,7 @@
+@@ -2298,7 +2065,7 @@
%% <p>The permission are not checked in this function.</p>
%% @todo We probably need to check that the user doing the query has the right
%% to read the items.
@@ -563,7 +598,7 @@
MaxItems =
if
SMaxItems == "" -> get_max_items_node(Host);
-@@ -2272,12 +2085,13 @@
+@@ -2312,12 +2079,13 @@
{error, Error} ->
{error, Error};
_ ->
@@ -578,7 +613,7 @@
{PresenceSubscription, RosterGroup} = get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups),
if
not RetreiveFeature ->
-@@ -2290,11 +2104,11 @@
+@@ -2330,11 +2098,11 @@
node_call(Type, get_items,
[NodeId, From,
AccessModel, PresenceSubscription, RosterGroup,
@@ -592,7 +627,7 @@
SendItems = case ItemIDs of
[] ->
Items;
-@@ -2307,7 +2121,8 @@
+@@ -2347,7 +2115,8 @@
%% number of items sent to MaxItems:
{result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}],
[{xmlelement, "items", nodeAttr(Node),
@@ -602,7 +637,7 @@
Error ->
Error
end
-@@ -2329,10 +2144,15 @@
+@@ -2369,10 +2138,15 @@
Error -> Error
end.
get_allowed_items_call(Host, NodeIdx, From, Type, Options, Owners) ->
@@ -619,13 +654,11 @@
%% @spec (Host, Node, NodeId, Type, LJID, Number) -> any()
-@@ -2344,31 +2164,29 @@
- %% Number = last | integer()
+@@ -2385,27 +2159,38 @@
%% @doc <p>Resend the items of a node to the user.</p>
%% @todo use cache-last-item feature
--send_items(Host, Node, NodeId, Type, {U,S,R} = LJID, last) ->
+ send_items(Host, Node, NodeId, Type, {U,S,R} = LJID, last) ->
- case get_cached_item(Host, NodeId) of
-+send_items(Host, Node, NodeId, Type, LJID, last) ->
+ Stanza = case get_cached_item(Host, NodeId) of
undefined ->
- send_items(Host, Node, NodeId, Type, LJID, 1);
@@ -660,39 +693,24 @@
- _ ->
- ok
- end
-- end
-- end;
--send_items(Host, Node, NodeId, Type, {U,S,R} = LJID, Number) ->
+ itemsEls([LastItem])}], ModifNow, ModifUSR)
+ end,
-+ ejabberd_router:route(service_jid(Host), jlib:make_jid(LJID), Stanza);
-+send_items(Host, Node, NodeId, Type, LJID, Number) ->
- ToSend = case node_action(Host, Type, get_items, [NodeId, LJID]) of
- {result, []} ->
- [];
-@@ -2391,20 +2209,7 @@
- [{xmlelement, "items", nodeAttr(Node),
- itemsEls(ToSend)}])
- end,
-- case is_tuple(Host) of
-- false ->
-- ejabberd_router:route(service_jid(Host), jlib:make_jid(LJID), Stanza);
-- true ->
-- case ejabberd_sm:get_session_pid(U,S,R) of
-- C2SPid when is_pid(C2SPid) ->
-- ejabberd_c2s:broadcast(C2SPid,
-- {pep_message, binary_to_list(Node)++"+notify"},
-- _Sender = service_jid(Host),
-- Stanza);
-- _ ->
-- ok
-- end
-- end.
-+ ejabberd_router:route(service_jid(Host), jlib:make_jid(LJID), Stanza).
-
- %% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response}
- %% Host = host()
-@@ -2540,7 +2345,8 @@
++ case is_tuple(Host) of
++ false ->
++ ejabberd_router:route(service_jid(Host), jlib:make_jid(LJID), Stanza);
++ true ->
++ case ejabberd_sm:get_session_pid(U,S,R) of
++ C2SPid when is_pid(C2SPid) ->
++ ejabberd_c2s:broadcast(C2SPid,
++ {pep_message, binary_to_list(Node)++"+notify"},
++ _Sender = service_jid(Host),
++ Stanza);
++ _ ->
++ ok
+ end
+ end;
+ send_items(Host, Node, NodeId, Type, {U,S,R} = LJID, Number) ->
+@@ -2580,7 +2365,8 @@
error ->
{error, ?ERR_BAD_REQUEST};
_ ->
@@ -702,7 +720,7 @@
case lists:member(Owner, Owners) of
true ->
OwnerJID = jlib:make_jid(Owner),
-@@ -2550,24 +2356,7 @@
+@@ -2590,24 +2376,7 @@
end,
lists:foreach(
fun({JID, Affiliation}) ->
@@ -728,21 +746,19 @@
end, FilteredEntities),
{result, []};
_ ->
-@@ -2620,11 +2409,11 @@
+@@ -2660,9 +2429,9 @@
end.
read_sub(Subscriber, Node, NodeID, SubID, Lang) ->
- case pubsub_subscription:get_subscription(Subscriber, NodeID, SubID) of
+ case pubsub_subscription_odbc:get_subscription(Subscriber, NodeID, SubID) of
- {error, notfound} ->
- {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")};
{result, #pubsub_subscription{options = Options}} ->
- {result, XdataEl} = pubsub_subscription:get_options_xform(Lang, Options),
+ {result, XdataEl} = pubsub_subscription_odbc:get_options_xform(Lang, Options),
OptionsEl = {xmlelement, "options", [{"jid", jlib:jid_to_string(Subscriber)},
{"subid", SubID}|nodeAttr(Node)],
[XdataEl]},
-@@ -2650,7 +2439,7 @@
+@@ -2694,7 +2463,7 @@
end.
set_options_helper(Configuration, JID, NodeID, SubID, Type) ->
@@ -751,16 +767,7 @@
{result, GoodSubOpts} -> GoodSubOpts;
_ -> invalid
end,
-@@ -2679,7 +2468,7 @@
- write_sub(_Subscriber, _NodeID, _SubID, invalid) ->
- {error, extended_error(?ERR_BAD_REQUEST, "invalid-options")};
- write_sub(Subscriber, NodeID, SubID, Options) ->
-- case pubsub_subscription:set_subscription(Subscriber, NodeID, SubID, Options) of
-+ case pubsub_subscription_odbc:set_subscription(Subscriber, NodeID, SubID, Options) of
- {error, notfound} ->
- {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")};
- {result, _} ->
-@@ -2847,8 +2636,8 @@
+@@ -2893,8 +2662,8 @@
{"subscription", subscription_to_string(Sub)} | nodeAttr(Node)], []}]}]},
ejabberd_router:route(service_jid(Host), jlib:make_jid(JID), Stanza)
end,
@@ -771,28 +778,27 @@
true ->
Result = lists:foldl(fun({JID, Subscription, SubId}, Acc) ->
-@@ -3203,7 +2992,7 @@
- {Depth, [{N, get_node_subs(N)} || N <- Nodes]}
- end, tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]))}
+@@ -3257,7 +3026,7 @@
+ Collection = tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]),
+ {result, [{Depth, [{N, sub_with_options(N)} || N <- Nodes]} || {Depth, Nodes} <- Collection]}
end,
- case transaction(Action, sync_dirty) of
+ case transaction(Host, Action, sync_dirty) of
- {result, CollSubs} -> CollSubs;
+ {result, CollSubs} -> subscribed_nodes_by_jid(NotifyType, CollSubs);
_ -> []
end.
-@@ -3217,9 +3006,9 @@
+@@ -3326,8 +3095,8 @@
+ []
+ end.
+ sub_with_options(JID, NodeId, SubId) ->
+- case pubsub_subscription:read_subscription(JID, NodeId, SubId) of
+- #pubsub_subscription{options = Options} -> {JID, SubId, Options};
++ case pubsub_subscription_odbc:get_subscription(JID, NodeId, SubId) of
++ {result, #pubsub_subscription{options = Options}} -> {JID, SubId, Options};
+ _ -> {JID, SubId, []}
+ end.
- get_options_for_subs(NodeID, Subs) ->
- lists:foldl(fun({JID, subscribed, SubID}, Acc) ->
-- case pubsub_subscription:read_subscription(JID, NodeID, SubID) of
-+ case pubsub_subscription_odbc:get_subscription(JID, NodeID, SubID) of
- {error, notfound} -> [{JID, SubID, []} | Acc];
-- #pubsub_subscription{options = Options} -> [{JID, SubID, Options} | Acc];
-+ {result, #pubsub_subscription{options = Options}} -> [{JID, SubID, Options} | Acc];
- _ -> Acc
- end;
- (_, Acc) ->
-@@ -3408,6 +3197,30 @@
+@@ -3403,6 +3172,30 @@
Result
end.
@@ -823,7 +829,7 @@
%% @spec (Host, Options) -> MaxItems
%% Host = host()
%% Options = [Option]
-@@ -3804,7 +3617,13 @@
+@@ -3799,7 +3592,13 @@
tree_action(Host, Function, Args) ->
?DEBUG("tree_action ~p ~p ~p",[Host,Function,Args]),
Fun = fun() -> tree_call(Host, Function, Args) end,
@@ -838,7 +844,7 @@
%% @doc <p>node plugin call.</p>
node_call(Type, Function, Args) ->
-@@ -3824,13 +3643,13 @@
+@@ -3819,13 +3618,13 @@
node_action(Host, Type, Function, Args) ->
?DEBUG("node_action ~p ~p ~p ~p",[Host,Type,Function,Args]),
@@ -854,7 +860,7 @@
case tree_call(Host, get_node, [Host, Node]) of
N when is_record(N, pubsub_node) ->
case Action(N) of
-@@ -3842,13 +3661,19 @@
+@@ -3837,13 +3636,19 @@
Error
end
end, Trans).
@@ -878,7 +884,7 @@
{result, Result} -> {result, Result};
{error, Error} -> {error, Error};
{atomic, {result, Result}} -> {result, Result};
-@@ -3856,6 +3681,15 @@
+@@ -3851,6 +3656,15 @@
{aborted, Reason} ->
?ERROR_MSG("transaction return internal error: ~p~n", [{aborted, Reason}]),
{error, ?ERR_INTERNAL_SERVER_ERROR};
@@ -894,7 +900,7 @@
{'EXIT', Reason} ->
?ERROR_MSG("transaction return internal error: ~p~n", [{'EXIT', Reason}]),
{error, ?ERR_INTERNAL_SERVER_ERROR};
-@@ -3864,6 +3698,17 @@
+@@ -3859,6 +3673,17 @@
{error, ?ERR_INTERNAL_SERVER_ERROR}
end.
diff --git a/src/mod_pubsub/pubsub_subscription.erl b/src/mod_pubsub/pubsub_subscription.erl
index 465945985..256bc97df 100644
--- a/src/mod_pubsub/pubsub_subscription.erl
+++ b/src/mod_pubsub/pubsub_subscription.erl
@@ -160,6 +160,8 @@ create_table() ->
Other -> Other
end.
+add_subscription(_JID, _NodeID, []) ->
+ make_subid();
add_subscription(_JID, _NodeID, Options) ->
SubID = make_subid(),
mnesia:write(#pubsub_subscription{subid = SubID, options = Options}),
@@ -174,11 +176,8 @@ read_subscription(_JID, _NodeID, SubID) ->
_ -> {error, notfound}
end.
-write_subscription(JID, NodeID, SubID, Options) ->
- case read_subscription(JID, NodeID, SubID) of
- {error, notfound} -> {error, notfound};
- Sub -> mnesia:write(Sub#pubsub_subscription{options = Options})
- end.
+write_subscription(_JID, _NodeID, SubID, Options) ->
+ mnesia:write(#pubsub_subscription{subid = SubID, options = Options}).
make_subid() ->
{T1, T2, T3} = now(),
diff --git a/src/mod_service_log.erl b/src/mod_service_log.erl
index 17f446a30..2ab2305a5 100644
--- a/src/mod_service_log.erl
+++ b/src/mod_service_log.erl
@@ -31,8 +31,8 @@
-export([start/2,
stop/1,
- log_user_send/3,
- log_user_receive/4]).
+ log_user_send/4,
+ log_user_receive/5]).
-include("ejabberd.hrl").
-include("jlib.hrl").
@@ -51,10 +51,10 @@ stop(Host) ->
?MODULE, log_user_receive, 50),
ok.
-log_user_send(From, To, Packet) ->
+log_user_send(_DebugFlag, From, To, Packet) ->
log_packet(From, To, Packet, From#jid.lserver).
-log_user_receive(_JID, From, To, Packet) ->
+log_user_receive(_DebugFlag, _JID, From, To, Packet) ->
log_packet(From, To, Packet, To#jid.lserver).
@@ -74,4 +74,3 @@ log_packet(From, To, {xmlelement, Name, Attrs, Els}, Host) ->
luser = "", lserver = Logger, lresource = ""},
{xmlelement, "route", [], [FixedPacket]})
end, Loggers).
-
diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl
index d5fa1e3b2..bee2f7600 100644
--- a/src/mod_shared_roster.erl
+++ b/src/mod_shared_roster.erl
@@ -365,7 +365,7 @@ in_subscription(Acc, User, Server, JID, Type, _Reason) ->
out_subscription(UserFrom, ServerFrom, JIDTo, unsubscribed) ->
%% Remove pending out subscription
#jid{luser = UserTo, lserver = ServerTo} = JIDTo,
- JIDFrom = jlib:make_jid(UserFrom, UserTo, ""),
+ JIDFrom = jlib:make_jid(UserFrom, ServerFrom, ""),
mod_roster:out_subscription(UserTo, ServerTo, JIDFrom, unsubscribe),
%% Remove pending in subscription
diff --git a/src/mod_support.erl b/src/mod_support.erl
new file mode 100644
index 000000000..bd3b8a01b
--- /dev/null
+++ b/src/mod_support.erl
@@ -0,0 +1,260 @@
+%%% ====================================================================
+%%% This software is copyright 2006-2010, ProcessOne.
+%%%
+%%% mod_support
+%%% allow automatic build of support archive to be sent to Process-One
+%%%
+%%% @copyright 2006-2010 ProcessOne
+%%% @author Christophe Romain <christophe.romain@process-one.net>
+%%% [http://www.process-one.net/]
+%%% @version {@vsn}, {@date} {@time}
+%%% @end
+%%% ====================================================================
+
+
+-module(mod_support).
+-author('christophe.romain@process-one.net').
+
+-behaviour(gen_mod).
+%-behaviour(gen_server).
+
+% module functions
+-export([start/2,stop/1,is_loaded/0,loop/1,dump/0]).
+-compile(export_all).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("licence.hrl").
+
+-include_lib("kernel/include/file.hrl").
+
+-define(LOG_FETCH_SIZE, 1000000).
+-define(RPC_TIMEOUT, 10000). % 10
+-define(MAX_FILE_SIZE, 2147483648). %%2Gb
+
+start(Host, Opts) ->
+ case ?IS_VALID of
+ true ->
+ case gen_mod:get_opt(dump_freq, Opts, 0) of
+ 0 -> no_dump;
+ Freq -> spawn(?MODULE, loop, [Freq*60000])
+ end,
+ ok;
+ false ->
+ not_started
+ end.
+
+stop(Host) ->
+ ok.
+
+is_loaded() ->
+ ok.
+
+loop(Timeout) ->
+ receive
+ quit -> ok
+ after Timeout ->
+ Dump = dump(),
+ BaseName = get_base_name(),
+ %%{Data,EjabberdLog,SaslLog,ECrash} = Dump,
+ write_logs(tuple_to_list(Dump),BaseName,["_memory.bin",
+ "_ejabberd.log.gz",
+ "_sasl.log.gz",
+ "_erlang_crash_dump.log.gz"]),
+ loop(Timeout)
+ end.
+
+get_base_name() ->
+ {{Y,M,D},{Hr,Mn,_Sc}} = calendar:local_time(),
+ case os:getenv("EJABBERD_LOG_PATH") of
+ false ->
+ filename:join(filename:dirname(filename:absname("")),
+ lists:flatten(io_lib:format("~b~b~b~b~b",[Y,M,D,Hr,Mn])));
+ Path ->
+ filename:join(filename:dirname(Path),
+ lists:flatten(io_lib:format("~b~b~b~b~b",[Y,M,D,Hr,Mn])))
+ end.
+
+write_logs([BinaryData|T],BaseName,[Filename|Filenames]) ->
+ Log = BaseName++Filename,
+ file:write_file(Log, BinaryData),
+ write_logs(T,BaseName,Filenames);
+
+write_logs([],BaseName,_)-> ok.
+
+dump() ->
+ Dump = lists:map(fun(LogFile) ->
+ Content = case file:open(LogFile,[read,raw]) of
+ {ok, IO} ->
+ Size = case file:read_file_info(LogFile) of
+ {ok, FileInfo} -> FileInfo#file_info.size;
+ _ -> ?LOG_FETCH_SIZE
+ end,
+ case Size>?MAX_FILE_SIZE of
+ true -> io_lib:format("File ~s is too big: ~p bytes.",[LogFile, Size]);
+ false ->
+ if Size>?LOG_FETCH_SIZE ->
+ file:position(IO, Size-?LOG_FETCH_SIZE),
+ case file:read(IO, ?LOG_FETCH_SIZE) of
+ {ok, Data1} -> Data1;
+ Error1 -> io_lib:format("can not read log file (~s): ~p",[LogFile, Error1])
+ end;
+ true ->
+ case file:read(IO, Size) of
+ {ok, Data2} -> Data2;
+ Error2 -> io_lib:format("can not read log file (~s): ~p",[LogFile, Error2])
+ end
+ end
+ end;
+ {error, Reason} ->
+ io_lib:format("can not open log file (~s): ~p",[LogFile, Reason])
+ end,
+ zlib:gzip(list_to_binary(Content))
+ end, [ejabberd_logs(), sasl_logs(), erl_crash()]),
+ NodeState = get_node_state(),
+ list_to_tuple([NodeState|Dump]).
+
+ejabberd_logs() ->
+ LogPath = case application:get_env(log_path) of
+ {ok, Path} ->
+ Path;
+ undefined ->
+ case os:getenv("EJABBERD_LOG_PATH") of
+ false -> ?LOG_PATH;
+ Path -> Path
+ end
+ end.
+
+sasl_logs() ->
+ case os:getenv("SASL_LOG_PATH") of
+ false -> filename:join([filename:dirname(ejabberd_logs()),"sasl.log"]);
+ Path -> Path
+ end.
+
+erl_crash() ->
+ LogsDir = filename:dirname(ejabberd_logs()),
+ CrashDumpWildcard = filename:join([LogsDir,"erl_crash*dump"]),
+ FileName = case filelib:wildcard(CrashDumpWildcard) of
+ [Files] -> [LastFile|T] = lists:reverse([Files]),
+ LastFile;
+ _ -> case os:getenv("ERL_CRASH_DUMP") of
+ false -> "erl_crash.dump";
+ Path -> Path
+ end
+ end.
+
+
+proc_info(Pid) ->
+ Info = process_info(Pid),
+ lists:map(fun(Elem) ->
+ List = proplists:get_value(Elem, Info),
+ {Elem, size(term_to_binary(List))}
+ end, [messages, dictionary])
+ ++ [X || X <- Info,
+ lists:member(element(1,X),
+ [heap_size,stack_size,reductions,links,status,initial_call,current_function])].
+
+environment() ->
+ {ok, KE} = application:get_key(kernel,env),
+ {ok, EE} = application:get_key(ejabberd,env),
+ Env = [{inetrc, os:getenv("ERL_INETRC")},
+ {sopath, os:getenv("EJABBERD_SO_PATH")},
+ {maxports, os:getenv("ERL_MAX_PORTS")},
+ {maxtables, os:getenv("ERL_MAX_ETS_TABLES")},
+ {crashdump, os:getenv("ERL_CRASH_DUMP")},
+ {archdir, os:getenv("ARCHDIR")},
+ {mnesia, mnesia:system_info(all)}],
+ Args = [{args, init:get_arguments()}, {plain, init:get_plain_arguments()}],
+ KE++EE++Env++Args.
+
+memtop(N) ->
+ E = lists:sublist(lists:reverse(lists:keysort(2,lists:map(fun(Tab) -> {Tab, ets:info(Tab,memory)} end, ets:all()))),N),
+ M = lists:sublist(lists:reverse(lists:keysort(2,lists:map(fun(Tab) -> {Tab, mnesia:table_info(Tab,memory)} end, mnesia:system_info(tables)))),N),
+ E++M.
+
+maxmsgqueue() ->
+ lists:max(lists:map(fun(Pid) -> proplists:get_value(message_queue_len,process_info(Pid)) end, erlang:processes())).
+
+msgqueue(N) ->
+ lists:filter(fun(L) -> proplists:get_value(message_queue_len, L) > N
+ end, lists:map(fun(Pid) -> process_info(Pid) end, erlang:processes())).
+
+%lists:sublist(lists:reverse(lists:keysort(2,lists:map(fun(Pid) -> {E,L} = process_info(Pid, dictionary), {E,length(L)} end, erlang:processes()))), 10)
+
+%%Entry point to invoke mod_support via command line.
+%%Example: erl -sname debug@localhost -s mod_support report ejabberd@localhost
+%%See issue #TECH-286.
+report(Node) ->
+ [NodeId|T]=Node,
+ UploadResult = force_load_code_into_node(NodeId, ?MODULE),
+ case UploadResult of
+ ok -> NodeState = rpc:call(NodeId,mod_support,get_node_state,[],?RPC_TIMEOUT),
+ Dump = rpc:call(NodeId,mod_support,dump,[],?RPC_TIMEOUT),
+ BaseName = get_base_name(),
+ %%{Data,EjabberdLog,SaslLog,ECrash} = Dump,
+ write_logs(tuple_to_list(Dump),BaseName,["_memory.bin",
+ "_ejabberd.log.gz",
+ "_sasl.log.gz",
+ "_erlang_crash_dump.log.gz"]),
+ error_logger:info_msg("State in node ~p was written to log~n",[NodeId]),
+ error_logger:info_msg("Unloading module ~s from node ~p. ",[?MODULE,NodeId]),
+ force_unload_code_from_node(NodeId, ?MODULE);
+ _ -> error_logger:info_msg("Error uploading module ~s from node ~p~n",[?MODULE,NodeId])
+ end.
+
+%%Load Module into the ejabberd Node specified.
+force_load_code_into_node(Node, Module) ->
+ CodeFile = code:where_is_file(atom_to_list(Module)++".beam"),
+ case file:read_file(CodeFile) of
+ {ok, Code} ->
+ rpc:call(Node, code, purge, [Module], ?RPC_TIMEOUT),
+ rpc:call(Node, code, delete, [Module], ?RPC_TIMEOUT),
+ case rpc:call(Node, code, load_binary, [Module, CodeFile, Code], ?RPC_TIMEOUT) of
+ {module, _} ->
+ error_logger:info_msg("Loading ~s module into ~p : success ~n", [Module,Node]),
+ rpc:block_call(Node, Module, is_loaded, [], ?RPC_TIMEOUT);
+ {error, badfile} ->
+ error_logger:info_msg("Loading ~s module into ~p : incorrect format ~n", [Module,Node]),
+ {error, badfile};
+ {error, not_purged} ->
+ % this should never happen anyway..
+ error_logger:info_msg("Loading ~s module into ~p : old code already exists ~n", [Module,Node]),
+ {error, not_purged};
+ {badrpc, Reason} ->
+ error_logger:info_msg("Loading ~s module into ~p: badrpc ~p ~n", [Module,Node,Reason]),
+ {badrpc, Reason}
+ end;
+ Error ->
+ error_logger:error_msg("Cannot read module file ~s ~p : ~p ~n", [Module, CodeFile, Error]),
+ Error
+ end.
+
+%%Unload erlang Module from the Node specified. Used to ensure cleanup after rpc calls.
+force_unload_code_from_node(Node, Module) ->
+ rpc:call(Node, code, purge, [Module], ?RPC_TIMEOUT),
+ rpc:call(Node, code, delete, [Module], ?RPC_TIMEOUT).
+
+%%Retrieve system state and pack it into Data
+%%TODO enhance state info. See #TECH-286.
+get_node_state() ->
+ Mem = erlang:memory(),
+ Ets = lists:map(fun(Tab) -> ets:info(Tab) end, ets:all()),
+ Mnesia = lists:map(fun(Tab) -> mnesia:table_info(Tab,all) end, mnesia:system_info(tables)),
+ Procs = lists:map(fun(Pid) -> proc_info(Pid) end, erlang:processes()),
+ Data = term_to_binary({Mem, Ets, Mnesia, Procs}).
+
+crash_dump() ->
+ SystemInfo = [erlang:system_info(X) || X<-[info,loaded,procs]],
+ [zlib:gzip(list_to_binary(lists:flatten(SystemInfo)))].
+
+crash_dump(Node) ->
+ [NodeId|T]=Node,
+ UploadResult = force_load_code_into_node(NodeId, ?MODULE),
+ case UploadResult of
+ ok -> Dump = rpc:call(NodeId,mod_support,crash_dump,[],?RPC_TIMEOUT),
+ BaseName = get_base_name(),
+ write_logs(Dump,BaseName,["_realtime_crash_dump.gz"]),
+ error_logger:info_msg("Unloading module ~s from node ~p. ",[?MODULE,NodeId]),
+ force_unload_code_from_node(NodeId, ?MODULE);
+ _ -> error_logger:info_msg("Error uploading module ~s from node ~p~n",[?MODULE,NodeId])
+ end.
diff --git a/src/mod_xmlrpc.erl b/src/mod_xmlrpc.erl
new file mode 100644
index 000000000..e5cdaf7d9
--- /dev/null
+++ b/src/mod_xmlrpc.erl
@@ -0,0 +1,1066 @@
+%%%----------------------------------------------------------------------
+%%% File : mod_xmlrpc.erl
+%%% Author : Badlop / Mickael Remond / Christophe Romain
+%%% Purpose : XML-RPC server
+%%% Created :
+%%% Id :
+%%%----------------------------------------------------------------------
+
+%%%/***************************************************************************
+%%% * *
+%%% * 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. *
+%%% * *
+%%% ***************************************************************************/
+%%%
+%%%
+%%% MOD_XMLRPC - an XML-RPC server module for ejabberd
+%%%
+%%% v0.5 - 17 March 2008
+%%%
+%%% http://ejabberd.jabber.ru/mod_xmlrpc
+%%%
+%%% (C) 2005, Badlop
+%%% 2006, Process-one
+%%% 2007, Process-one
+%%% 2008, Process-one
+%%%
+%%% Changelog:
+%%%
+%%% 0.7 - 02 April 2009 - cromain
+%%% - add user nick change
+%%%
+%%% 0.6 - 02 June 2008 - cromain
+%%% - add user existance checking
+%%% - improve parameter checking
+%%% - allow orderless parameter
+%%%
+%%% 0.5 - 17 March 2008 - cromain
+%%% - add user changing and higher level methods
+%%%
+%%% 0.4 - 18 February 2008 - cromain
+%%% - add roster handling
+%%% - add message sending
+%%% - code and api clean-up
+%%%
+%%% 0.3 - 18 October 2007 - cromain
+%%% - presence improvement
+%%% - add new functionality
+%%%
+%%% 0.2 - 4 March 2006 - mremond
+%%% - Code clean-up
+%%% - Made it compatible with current ejabberd SVN version
+%%%
+%%% 0.1.2 - 28 December 2005
+%%% - Now compatible with ejabberd 1.0.0
+%%% - The XMLRPC server is started only once, not once for every virtual host
+%%% - Added comments for handlers. Every available handler must be explained
+%%%
+
+-module(mod_xmlrpc).
+-author('Process-one').
+-vsn('0.6').
+
+-behaviour(gen_mod).
+
+-export([start/2,
+ handler/2,
+ link_contacts/5,
+ unlink_contacts/3,
+ loop/1,
+ stop/1]).
+
+-export([add_rosteritem/6]).
+-export([add_rosteritem_groups/5,
+ del_rosteritem_groups/5,
+ modify_rosteritem_groups/7]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("mod_roster.hrl").
+
+-ifdef(EJABBERD1).
+-record(session, {sid, usr, us, priority}). %% ejabberd 1.1.x
+-else.
+-record(session, {sid, usr, us, priority, info}). %% ejabberd 2.0.x
+-endif.
+
+
+-define(PROCNAME, ejabberd_mod_xmlrpc).
+-define(PORT, 4560).
+-define(TIMEOUT, 5000).
+
+%% -----------------------------
+%% Module interface
+%% -----------------------------
+
+start(_Host, Opts) ->
+ case whereis(?PROCNAME) of
+ undefined ->
+ %% get options
+ Port = gen_mod:get_opt(port, Opts, ?PORT),
+ MaxSessions = 10,
+ Timeout = gen_mod:get_opt(timeout, Opts, ?TIMEOUT),
+ Handler = {mod_xmlrpc, handler},
+ State = tryit,
+
+ %% TODO: this option gives
+ %% error_info: {function_clause,[{gen_tcp,mod,[{ip,{127,0,0,1}}]},
+ %%case gen_mod:get_opt(listen_all, Opts, false) of
+ %% true -> Ip = all;
+ %% false -> Ip = {127, 0, 0, 1}
+ %%end,
+ Ip = all,
+
+ %% start the XML-RPC server
+ {ok, Pid} = xmlrpc:start_link(Ip, Port, MaxSessions, Timeout, Handler, State),
+
+ %% start the loop process
+ register(?PROCNAME, spawn(?MODULE, loop, [Pid])),
+ ok;
+ _ ->
+ ok
+ end.
+
+loop(Pid) ->
+ receive
+ stop ->
+ xmlrpc:stop(Pid)
+ end.
+
+stop(_Host) ->
+ case whereis(?PROCNAME) of
+ undefined ->
+ ok;
+ _Pid ->
+ ?PROCNAME ! stop,
+ unregister(?PROCNAME)
+ end.
+
+
+%% -----------------------------
+%% Handlers
+%% -----------------------------
+
+handler(tryit, Call) ->
+ try handler(notry, Call) of
+ Result -> Result
+ catch
+ A:B ->
+ ?ERROR_MSG("Problem '~p' in~nCall: ~p~nError: ~p", [A, Call, B]),
+ {false, {response, [-100]}}
+ end;
+
+% Call: Arguments: Returns:
+
+%% .............................
+%% Debug
+
+%% echothis String String
+handler(_State, {call, echothis, [A]}) ->
+ {false, {response, [A]}};
+
+%% multhis struct[{a, Integer}, {b, Integer}] Integer
+handler(_State, {call, multhis, [{struct, Struct}]}) ->
+ [{a, A}, {b, B}] = lists:sort(Struct),
+ {false, {response, [A*B]}};
+
+%% .............................
+%% User administration
+
+%% create_account struct[{user, String}, {server, Server}, {password, String}] Integer
+handler(_State, {call, create_account, [{struct, Struct}]}) ->
+ [{password, P}, {server, S}, {user, U}] = lists:sort(Struct),
+ case ejabberd_auth:try_register(U, S, P) of
+ {atomic, ok} ->
+ {false, {response, [0]}};
+ {atomic, exists} ->
+ {false, {response, [409]}};
+ _ ->
+ {false, {response, [1]}}
+ end;
+
+%% delete_account struct[{user, String}, {server, Server}] Integer
+handler(_State, {call, delete_account, [{struct, Struct}]}) ->
+ [{server, S}, {user, U}] = lists:sort(Struct),
+ Fun = fun() -> ejabberd_auth:remove_user(U, S) end,
+ user_action(U, S, Fun, ok);
+
+%% change_password struct[{user, String}, {server, String}, {newpass, String}] Integer
+handler(_State, {call, change_password, [{struct, Struct}]}) ->
+ [{newpass, P}, {server, S}, {user, U}] = lists:sort(Struct),
+ Fun = fun() -> ejabberd_auth:set_password(U, S, P) end,
+ user_action(U, S, Fun, ok);
+
+%% set_nickname struct[{user, String}, {server, String}, {nick, String}] Integer
+handler(_State, {call, set_nickname, [{struct, Struct}]}) ->
+ [{nick, N}, {server, S}, {user, U}] = lists:sort(Struct),
+ Fun = fun() -> case mod_vcard:process_sm_iq(
+ {jid, U, S, "", U, S, ""},
+ {jid, U, S, "", U, S, ""},
+ {iq, "", set, "", "en",
+ {xmlelement, "vCard",
+ [{"xmlns", "vcard-temp"}], [
+ {xmlelement, "NICKNAME", [], [{xmlcdata, N}]}
+ ]
+ }}) of
+ {iq, [], result, [], _, []} -> ok;
+ _ -> error
+ end
+ end,
+ user_action(U, S, Fun, ok);
+
+%% set_rosternick struct[{user, String}, {server, String}, {nick, String}] Integer
+handler(_State, {call, set_rosternick, [{struct, Struct}]}) ->
+ [{nick, N}, {server, S}, {user, U}] = lists:sort(Struct),
+ Fun = fun() -> change_rosternick(U, S, N) end,
+ user_action(U, S, Fun, ok);
+
+%% add_rosteritem struct[{user, String}, {server, String},
+%% {jid, String}, {group, String}, {nick, String}, {subs, String}] Integer
+handler(_State, {call, add_rosteritem, [{struct, Struct}]}) ->
+ [{group, G},{jid, JID},{nick, N},{server, S},{subs, Subs},{user, U}] = lists:sort(Struct),
+ Fun = fun() -> add_rosteritem(U, S, JID, N, G, Subs) end,
+ user_action(U, S, Fun, {atomic, ok});
+
+%% link_contacts struct[{jid1, String}, {nick1, String}, {jid2, String}, {nick2, String}] Integer
+handler(_State, {call, link_contacts, [{struct, Struct}]}) ->
+ [{group1, G1}, {group2, G2}, {jid1, JID1}, {jid2, JID2}, {nick1, Nick1}, {nick2, Nick2}] = lists:sort(Struct),
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case {ejabberd_auth:is_user_exists(U1, S1), ejabberd_auth:is_user_exists(U2, S2)} of
+ {true, true} ->
+ case link_contacts(JID1, Nick1, G1, JID2, Nick2, G2) of
+ {atomic, ok} ->
+ {false, {response, [0]}};
+ _ ->
+ {false, {response, [1]}}
+ end;
+ _ ->
+ {false, {response, [404]}}
+ end;
+
+%% delete_rosteritem struct[{user, String}, {server, String}, {jid, String}] Integer
+handler(_State, {call, delete_rosteritem, [{struct, Struct}]}) ->
+ [{jid, JID}, {server, S}, {user, U}] = lists:sort(Struct),
+ Fun = fun() -> del_rosteritem(U, S, JID) end,
+ user_action(U, S, Fun, {atomic, ok});
+
+%% unlink_contacts struct[{jid1, String}, {jid2, String}] Integer
+handler(_State, {call, unlink_contacts, [{struct, Struct}]}) ->
+ [{jid1, JID1}, {jid2, JID2}] = lists:sort(Struct),
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case {ejabberd_auth:is_user_exists(U1, S1), ejabberd_auth:is_user_exists(U2, S2)} of
+ {true, true} ->
+ case unlink_contacts(JID1, JID2) of
+ {atomic, ok} ->
+ {false, {response, [0]}};
+ _ ->
+ {false, {response, [1]}}
+ end;
+ _ ->
+ {false, {response, [404]}}
+ end;
+
+handler(_State, {call, add_rosteritem_groups, [{struct, Struct}]}) ->
+ [{jid, JID}, {newgroups, NewGroupsString}, {push, PushString}, {server, Server}, {user, User}] = lists:sort(Struct),
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ NewGroups = string:tokens(NewGroupsString, ";"),
+ Push = list_to_atom(PushString),
+ case {ejabberd_auth:is_user_exists(U1, S1), ejabberd_auth:is_user_exists(User, Server)} of
+ {true, true} ->
+ case add_rosteritem_groups(User, Server, JID, NewGroups, Push) of
+ ok ->
+ {false, {response, [0]}};
+ Error ->
+ ?INFO_MSG("Error found: ~n~p", [Error]),
+ {false, {response, [1]}}
+ end;
+ _ ->
+ {false, {response, [404]}}
+ end;
+
+handler(_State, {call, del_rosteritem_groups, [{struct, Struct}]}) ->
+ [{jid, JID}, {newgroups, NewGroupsString}, {push, PushString}, {server, Server}, {user, User}] = lists:sort(Struct),
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ NewGroups = string:tokens(NewGroupsString, ";"),
+ Push = list_to_atom(PushString),
+ case {ejabberd_auth:is_user_exists(U1, S1), ejabberd_auth:is_user_exists(User, Server)} of
+ {true, true} ->
+ case del_rosteritem_groups(User, Server, JID, NewGroups, Push) of
+ ok ->
+ {false, {response, [0]}};
+ Error ->
+ ?INFO_MSG("Error found: ~n~p", [Error]),
+ {false, {response, [1]}}
+ end;
+ _ ->
+ {false, {response, [404]}}
+ end;
+
+handler(_State, {call, modify_rosteritem_groups, [{struct, Struct}]}) ->
+ [{jid, JID}, {newgroups, NewGroupsString}, {nick, Nick}, {push, PushString},
+ {server, Server}, {subscription, SubsString}, {user, User}] = lists:sort(Struct),
+ Subs = list_to_atom(SubsString),
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ NewGroups = string:tokens(NewGroupsString, ";"),
+ Push = list_to_atom(PushString),
+ case ejabberd_auth:is_user_exists(User, Server) of
+ true ->
+ case modify_rosteritem_groups(User, Server, JID, NewGroups, Push, Nick, Subs) of
+ ok ->
+ {false, {response, [0]}};
+ Error ->
+ ?INFO_MSG("Error found: ~n~p", [Error]),
+ {false, {response, [1]}}
+ end;
+ _ ->
+ {false, {response, [404]}}
+ end;
+
+%% get_roster struct[{user, String}, {server, String}]
+%% array[struct[{jid, String}, {groups, array[String]}, {nick, String},
+%% {subscription, String}, {pending, String}]]
+handler(_State, {call, get_roster, [{struct, Struct}]}) ->
+ [{server, S}, {user, U}] = lists:sort(Struct),
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ Roster = format_roster(get_roster(U, S)),
+ {false, {response, [{array, Roster}]}};
+ false ->
+ {false, {response, [404]}}
+ end;
+
+%% get_roster_with_presence struct[{user, String}, {server, String}]
+%% array[struct[{jid, String}, {resource, String}, {group, String}, {nick, String},
+%% {subscription, String}, {pending, String},
+%% {show, String}, {status, String}]]
+handler(_State, {call, get_roster_with_presence, [{struct, Struct}]}) ->
+ [{server, S}, {user, U}] = lists:sort(Struct),
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ Roster = format_roster_with_presence(get_roster(U, S)),
+ {false, {response, [{array, Roster}]}};
+ false ->
+ {false, {response, [404]}}
+ end;
+
+%% get_presence struct[{user, String}, {server, String}]
+%% array[struct[{jid, String}, {show, String}, {status, String}]]
+handler(_State, {call, get_presence, [{struct, Struct}]}) ->
+ [{server, S}, {user, U}] = lists:sort(Struct),
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ {Resource, Show, Status} = get_presence(U, S),
+ FullJID = case Resource of
+ [] ->
+ lists:flatten([U,"@",S]);
+ _ ->
+ lists:flatten([U,"@",S,"/",Resource])
+ end,
+ R = {struct, [{jid, FullJID}, {show, Show}, {status, Status} ]},
+ {false, {response, [R]}};
+ false ->
+ {false, {response, [404]}}
+ end;
+
+%% get_resources struct[{user, String}, {server, String}]
+%% array[String]
+handler(_State, {call, get_resources, [{struct, Struct}]}) ->
+ [{server, S}, {user, U}] = lists:sort(Struct),
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ Resources = get_resources(U, S),
+ {false, {response, [{array, Resources}]}};
+ false ->
+ {false, {response, [404]}}
+ end;
+
+%% send_chat struct[{from, String}, {to, String}, {body, String}]
+%% Integer
+handler(_State, {call, send_chat, [{struct, Struct}]}) ->
+ [{body, Msg}, {from, FromJID}, {to, ToJID}] = lists:sort(Struct),
+ From = jlib:string_to_jid(FromJID),
+ To = jlib:string_to_jid(ToJID),
+ Stanza = {xmlelement, "message", [{"type", "chat"}],
+ [{xmlelement, "body", [], [{xmlcdata, Msg}]}]},
+ ejabberd_router:route(From, To, Stanza),
+ {false, {response, [0]}};
+
+%% send_message struct[{from, String}, {to, String}, {subject, String}, {body, String}]
+%% Integer
+handler(_State, {call, send_message, [{struct, Struct}]}) ->
+ [{body, Msg}, {from, FromJID}, {subject, Sub}, {to, ToJID}] = lists:sort(Struct),
+ From = jlib:string_to_jid(FromJID),
+ To = jlib:string_to_jid(ToJID),
+ Stanza = {xmlelement, "message", [{"type", "normal"}],
+ [{xmlelement, "subject", [], [{xmlcdata, Sub}]},
+ {xmlelement, "body", [], [{xmlcdata, Msg}]}]},
+ ejabberd_router:route(From, To, Stanza),
+ {false, {response, [0]}};
+
+%% send_stanza struct[{from, String}, {to, String}, {stanza, String}]
+%% Integer
+handler(_State, {call, send_stanza, [{struct, Struct}]}) ->
+ [{from, FromJID}, {stanza, StanzaStr}, {to, ToJID}] = lists:sort(Struct),
+ case xml_stream:parse_element(StanzaStr) of
+ {error, _} ->
+ {false, {response, [1]}};
+ Stanza ->
+ {xmlelement, _, Attrs, _} = Stanza,
+ From = jlib:string_to_jid(proplists:get_value("from", Attrs, FromJID)),
+ To = jlib:string_to_jid(proplists:get_value("to", Attrs, ToJID)),
+ ejabberd_router:route(From, To, Stanza),
+ {false, {response, [0]}}
+ end;
+
+%% rename_account struct[{user, String}, {server, String}, {newuser, String}, {newserver, String}]
+%% Integer
+handler(_State, {call, rename_account, [{struct, Struct}]}) ->
+ [{newserver, NS}, {newuser, NU}, {server, S}, {user, U}] = lists:sort(Struct),
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ case ejabberd_auth:get_password(U, S) of
+ false ->
+ {false, {response, [1]}};
+ Password ->
+ case ejabberd_auth:try_register(NU, NS, Password) of
+ {atomic, ok} ->
+ OldJID = jlib:jid_to_string({U, S, ""}),
+ NewJID = jlib:jid_to_string({NU, NS, ""}),
+ Roster = get_roster(U, S),
+ lists:foreach(fun(#roster{jid={RU, RS, RE}, name=Nick, groups=Groups}) ->
+ NewGroup = extract_group(Groups),
+ {NewNick, Group} = case lists:filter(fun(#roster{jid={PU, PS, _}}) ->
+ (PU == U) and (PS == S)
+ end, get_roster(RU, RS)) of
+ [#roster{name=OldNick, groups=OldGroups}|_] -> {OldNick, extract_group(OldGroups)};
+ [] -> {NU, []}
+ end,
+ JIDStr = jlib:jid_to_string({RU, RS, RE}),
+ link_contacts(NewJID, NewNick, NewGroup, JIDStr, Nick, Group),
+ unlink_contacts(OldJID, JIDStr)
+ end, Roster),
+ ejabberd_auth:remove_user(U, S),
+ {false, {response, [0]}};
+ {atomic, exists} ->
+ {false, {response, [409]}};
+ _ ->
+ {false, {response, [1]}}
+ end
+ end;
+ false ->
+ {false, {response, [404]}}
+ end;
+
+%% add_contacts struct[{user, String}, {server, String},
+%% array[struct[{jid, String}, {group, String}, {nick, String}]]]
+%% Integer
+handler(_State, {call, add_contacts, [{struct, Struct}]}) ->
+ [{array, Contacts}, {server, S}, {user, U}] = lists:sort(Struct),
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ JID1 = jlib:jid_to_string({U, S, ""}),
+ Response = lists:foldl(fun({struct, Struct2}, Acc) ->
+ [{group, Group}, {jid, JID2}, {nick, Nick}] = lists:sort(Struct2),
+ {PU, PS, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case ejabberd_auth:is_user_exists(PU, PS) of
+ true ->
+ case link_contacts(JID1, "", "", JID2, Nick, Group) of
+ {atomic, ok} -> Acc;
+ _ -> 1
+ end;
+ false ->
+ Acc
+ end
+ end, 0, element(2, Contacts)),
+ {false, {response, [Response]}};
+ false ->
+ {false, {response, [404]}}
+ end;
+
+%% remove_contacts struct[{user, String}, {server, String}, array[String]]
+%% Integer
+handler(_State, {call, remove_contacts, [{struct, Struct}]}) ->
+ [{array, Contacts}, {server, S}, {user, U}] = lists:sort(Struct),
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ JID1 = jlib:jid_to_string({U, S, ""}),
+ Response = lists:foldl(fun(JID2, Acc) ->
+ {PU, PS, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case ejabberd_auth:is_user_exists(PU, PS) of
+ true ->
+ case unlink_contacts(JID1, JID2) of
+ {atomic, ok} -> Acc;
+ _ -> 1
+ end;
+ false ->
+ Acc
+ end
+ end, 0, element(2, Contacts)),
+ {false, {response, [Response]}};
+ false ->
+ {false, {response, [404]}}
+ end;
+
+%% check_users_registration array[struct[{user, String}, {server, String}]]
+%% array[struct[{user, String}, {server, String}, {status, Integer}]]
+handler(_State, {call, check_users_registration, [{array, Users}]}) ->
+ Response = lists:map(fun({struct, Struct}) ->
+ [{server, S}, {user, U}] = lists:sort(Struct),
+ Registered = case ejabberd_auth:is_user_exists(U, S) of
+ true -> 1;
+ false -> 0
+ end,
+ {struct, [{user, U}, {server, S}, {status, Registered}]}
+ end, Users),
+ {false, {response, [{array, Response}]}};
+
+
+%% If no other guard matches
+handler(_State, Payload) ->
+ FaultString = lists:flatten(io_lib:format("Unknown call: ~p", [Payload])),
+ {false, {response, {fault, -1, FaultString}}}.
+
+
+%% -----------------------------
+%% Internal roster handling
+%% -----------------------------
+
+get_roster(User, Server) ->
+ LUser = jlib:nodeprep(User),
+ LServer = jlib:nameprep(Server),
+ ejabberd_hooks:run_fold(roster_get, LServer, [], [{LUser, LServer}]).
+
+change_rosternick(User, Server, Nick) ->
+ LUser = jlib:nodeprep(User),
+ LServer = jlib:nameprep(Server),
+ LJID = {LUser, LServer, []},
+ JID = jlib:jid_to_string(LJID),
+ Push = fun(Subscription) ->
+ jlib:iq_to_xml(#iq{type = set, xmlns = ?NS_ROSTER, id = "push",
+ sub_el = [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}],
+ [{xmlelement, "item", [{"jid", JID}, {"name", Nick}, {"subscription", atom_to_list(Subscription)}],
+ []}]}]})
+ end,
+ Result = case roster_backend(Server) of
+ mnesia ->
+ %% XXX This way of doing can not work with s2s
+ mnesia:transaction(
+ fun() ->
+ lists:foreach(fun(Roster) ->
+ {U, S} = Roster#roster.us,
+ mnesia:write(Roster#roster{name = Nick}),
+ lists:foreach(fun(R) ->
+ UJID = jlib:make_jid(U, S, R),
+ ejabberd_router:route(UJID, UJID, Push(Roster#roster.subscription))
+ end, get_resources(U, S))
+ end, mnesia:match_object(#roster{jid = LJID, _ = '_'}))
+ end);
+ odbc ->
+ %%% XXX This way of doing does not work with several domains
+ ejabberd_odbc:sql_transaction(Server,
+ fun() ->
+ SNick = ejabberd_odbc:escape(Nick),
+ SJID = ejabberd_odbc:escape(JID),
+ ejabberd_odbc:sql_query_t(
+ ["update rosterusers"
+ " set nick='", SNick, "'"
+ " where jid='", SJID, "';"]),
+ case ejabberd_odbc:sql_query_t(
+ ["select username from rosterusers"
+ " where jid='", SJID, "'"
+ " and subscription = 'B';"]) of
+ {selected, ["username"], Users} ->
+ lists:foreach(fun({RU}) ->
+ lists:foreach(fun(R) ->
+ UJID = jlib:make_jid(RU, Server, R),
+ ejabberd_router:route(UJID, UJID, Push(both))
+ end, get_resources(RU, Server))
+ end, Users);
+ _ ->
+ ok
+ end
+ end);
+ none ->
+ {error, no_roster}
+ end,
+ case Result of
+ {atomic, ok} -> ok;
+ _ -> error
+ end.
+
+add_rosteritem(User, Server, JID, Nick, Group, Subscription) ->
+ add_rosteritem(User, Server, JID, Nick, Group, Subscription, true).
+add_rosteritem(User, Server, JID, Nick, Group, Subscription, Push) ->
+ {RU, RS, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ LJID = {RU,RS,[]},
+ Groups = case Group of
+ [] -> [];
+ _ -> [Group]
+ end,
+ Roster = #roster{
+ usj = {User,Server,LJID},
+ us = {User,Server},
+ jid = LJID,
+ name = Nick,
+ ask = none,
+ subscription = list_to_atom(Subscription),
+ groups = Groups},
+ Result =
+ case roster_backend(Server) of
+ mnesia ->
+ mnesia:transaction(fun() ->
+ case mnesia:read({roster,{User,Server,LJID}}) of
+ [#roster{subscription=both}] ->
+ already_added;
+ _ ->
+ mnesia:write(Roster)
+ end
+ end);
+ odbc ->
+ %% MREMOND: TODO: check if already_added
+ case ejabberd_odbc:sql_transaction(Server,
+ fun() ->
+ Username = ejabberd_odbc:escape(User),
+ SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)),
+ case ejabberd_odbc:sql_query_t(
+ ["select username from rosterusers "
+ " where username='", Username, "' "
+ " and jid='", SJID,
+ "' and subscription = 'B';"]) of
+ {selected, ["username"],[]} ->
+ ItemVals = record_to_string(Roster),
+ ItemGroups = groups_to_string(Roster),
+ odbc_queries:update_roster(Server, Username,
+ SJID, ItemVals,
+ ItemGroups);
+ _ ->
+ already_added
+ end
+ end) of
+ {atomic, already_added} -> {atomic, already_added};
+ {atomic, _} -> {atomic, ok};
+ Error -> Error
+ end;
+ none ->
+ %% Apollo change: force roster push anyway with success
+ {atomic, ok}
+ end,
+ case {Result, Push} of
+ {{atomic, already_added}, _} -> ok; %% No need for roster push
+ {{atomic, ok}, true} -> roster_push(User, Server, JID, Nick, Subscription);
+ {{error, no_roster}, true} -> roster_push(User, Server, JID, Nick, Subscription);
+ {{atomic, ok}, false} -> ok;
+ _ -> error
+ end,
+ Result.
+
+del_rosteritem(User, Server, JID) ->
+ del_rosteritem(User, Server, JID, true).
+del_rosteritem(User, Server, JID, Push) ->
+ {RU, RS, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ LJID = {RU,RS,[]},
+ Result = case roster_backend(Server) of
+ mnesia ->
+ mnesia:transaction(fun() ->
+ mnesia:delete({roster, {User,Server,LJID}})
+ end);
+ odbc ->
+ case ejabberd_odbc:sql_transaction(Server, fun() ->
+ Username = ejabberd_odbc:escape(User),
+ SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)),
+ odbc_queries:del_roster(Server, Username, SJID)
+ end) of
+ {atomic, _} -> {atomic, ok};
+ Error -> Error
+ end;
+ none ->
+ %% Apollo change: force roster push anyway with success
+ {atomic, ok}
+ end,
+ case {Result, Push} of
+ {{atomic, ok}, true} -> roster_push(User, Server, JID, "", "remove");
+ %{{error, no_roster}, true} -> roster_push(User, Server, JID, "", "remove");
+ {{atomic, ok}, false} -> ok;
+ _ -> error
+ end,
+ Result.
+
+link_contacts(JID1, Nick1, JID2, Nick2) ->
+ link_contacts(JID1, Nick1, JID2, Nick2, true).
+link_contacts(JID1, Nick1, JID2, Nick2, Push) ->
+ link_contacts(JID1, Nick1, [], JID2, Nick2, [], Push).
+
+link_contacts(JID1, Nick1, Group1, JID2, Nick2, Group2) ->
+ link_contacts(JID1, Nick1, Group1, JID2, Nick2, Group2, true).
+link_contacts(JID1, Nick1, Group1, JID2, Nick2, Group2, Push) ->
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case add_rosteritem(U1, S1, JID2, Nick2, Group1, "both", Push) of
+ {atomic, ok} -> add_rosteritem(U2, S2, JID1, Nick1, Group2, "both", Push);
+ Error -> Error
+ end.
+
+unlink_contacts(JID1, JID2) ->
+ unlink_contacts(JID1, JID2, true).
+unlink_contacts(JID1, JID2, Push) ->
+ {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case del_rosteritem(U1, S1, JID2, Push) of
+ {atomic, ok} -> del_rosteritem(U2, S2, JID1, Push);
+ Error -> Error
+ end.
+
+add_rosteritem_groups(User, Server, JID, NewGroups, Push) ->
+ GroupsFun =
+ fun(Groups) ->
+ lists:usort(NewGroups ++ Groups)
+ end,
+ change_rosteritem_group(User, Server, JID, GroupsFun, Push).
+
+del_rosteritem_groups(User, Server, JID, NewGroups, Push) ->
+ GroupsFun =
+ fun(Groups) ->
+ Groups -- NewGroups
+ end,
+ change_rosteritem_group(User, Server, JID, GroupsFun, Push).
+
+modify_rosteritem_groups(User, Server, JID2, NewGroups, Push, Nick, Subs) when NewGroups == [] ->
+ JID1 = jlib:jid_to_string(jlib:make_jid(User, Server, "")),
+ case unlink_contacts(JID1, JID2) of
+ {atomic, ok} ->
+ ok;
+ Error ->
+ Error
+ end;
+modify_rosteritem_groups(User, Server, JID, NewGroups, Push, Nick, Subs) ->
+ GroupsFun =
+ fun(_Groups) ->
+ NewGroups
+ end,
+ change_rosteritem_group(User, Server, JID, GroupsFun, Push, NewGroups, Nick, Subs).
+
+change_rosteritem_group(User, Server, JID, GroupsFun, Push) ->
+ change_rosteritem_group(User, Server, JID, GroupsFun, Push, [], "unknownnickname", "both").
+
+change_rosteritem_group(User, Server, JID, GroupsFun, Push, NewGroups, Nick, Subs) ->
+ {RU, RS, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ LJID = {RU,RS,[]},
+ LUser = jlib:nodeprep(User),
+ LServer = jlib:nameprep(Server),
+ Result =
+ case roster_backend(LServer) of
+ mnesia ->
+ mnesia:transaction(
+ fun() ->
+ case mnesia:read({roster, {LUser, LServer, LJID}}) of
+ [#roster{} = Roster] ->
+ NewGroups = GroupsFun(Roster#roster.groups),
+ NewRoster = Roster#roster{groups = NewGroups},
+ mnesia:write(NewRoster),
+ {ok, NewRoster#roster.name,
+ NewRoster#roster.subscription,
+ NewGroups};
+ _ ->
+ not_in_roster
+ end
+ end);
+ odbc ->
+ ejabberd_odbc:sql_transaction(
+ LServer,
+ fun() ->
+ Username = ejabberd_odbc:escape(User),
+ SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)),
+ case ejabberd_odbc:sql_query_t(
+ ["select nick, subscription from rosterusers "
+ " where username='", Username, "' "
+ " and jid='", SJID, "';"]) of
+ {selected, ["nick", "subscription"],
+ [{Name, SSubscription}]} ->
+ Subscription =
+ case SSubscription of
+ "B" -> both;
+ "T" -> to;
+ "F" -> from;
+ _ -> none
+ end,
+ Groups =
+ case odbc_queries:get_roster_groups(
+ LServer, Username, SJID) of
+ {selected, ["grp"], JGrps}
+ when is_list(JGrps) ->
+ [JGrp || {JGrp} <- JGrps];
+ _ ->
+ []
+ end,
+ NewGroups = GroupsFun(Groups),
+ ejabberd_odbc:sql_query_t(
+ ["delete from rostergroups "
+ " where username='", Username, "' "
+ " and jid='", SJID, "';"]),
+ lists:foreach(
+ fun(Group) ->
+ ejabberd_odbc:sql_query_t(
+ ["insert into rostergroups("
+ " username, jid, grp) "
+ " values ('", Username, "',"
+ "'", SJID, "',"
+ "'", ejabberd_odbc:escape(Group), "');"])
+ end,
+ NewGroups),
+ {ok, Name, Subscription, NewGroups};
+ _ ->
+ not_in_roster
+ end
+ end);
+ none ->
+ %% Apollo change: force roster push anyway with success
+ {atomic, {ok, Nick, Subs, NewGroups}}
+ end,
+ case {Result, Push} of
+ {{atomic, {ok, Name, Subscription, NewGroups}}, true} ->
+ roster_push(User, Server, JID,
+ Name, atom_to_list(Subscription), NewGroups),
+ ok;
+ {{atomic, {ok, _Name, _Subscription, _NewGroups}}, false} -> ok;
+ {{atomic, not_in_roster}, _} -> not_in_roster;
+ Error -> {error, Error}
+ end.
+
+roster_push(User, Server, JID, Nick, Subscription) ->
+ roster_push(User, Server, JID, Nick, Subscription, []).
+
+roster_push(User, Server, JID, Nick, Subscription, Groups) ->
+ LJID = jlib:make_jid(User, Server, ""),
+ TJID = jlib:string_to_jid(JID),
+ {TU, TS, _} = jlib:jid_tolower(TJID),
+ Presence = {xmlelement, "presence", [{"type",
+ case Subscription of
+ "remove" -> "unsubscribed";
+ "none" -> "unsubscribe";
+ "both" -> "subscribed";
+ _ -> "subscribe"
+ end}], []},
+ ItemAttrs = case Nick of
+ "" -> [{"jid", JID}, {"subscription", Subscription}];
+ _ -> [{"jid", JID}, {"name", Nick}, {"subscription", Subscription}]
+ end,
+ ItemGroups =
+ lists:map(fun(G) ->
+ {xmlelement, "group", [], [{xmlcdata, G}]}
+ end, Groups),
+ Result =
+ jlib:iq_to_xml(
+ #iq{type = set, xmlns = ?NS_ROSTER, id = "push",
+ sub_el = [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}],
+ [{xmlelement, "item", ItemAttrs, ItemGroups}]}]}),
+ %% ejabberd_router:route(TJID, LJID, Presence),
+ %% ejabberd_router:route(LJID, LJID, Result),
+ lists:foreach(
+ fun(Resource) ->
+ UJID = jlib:make_jid(User, Server, Resource),
+ ejabberd_router:route(TJID, UJID, Presence),
+ ejabberd_router:route(UJID, UJID, Result),
+ case Subscription of
+ "remove" -> none;
+ _ ->
+ lists:foreach(
+ fun(TR) ->
+ ejabberd_router:route(
+ jlib:make_jid(TU, TS, TR), UJID,
+ {xmlelement, "presence", [], []})
+ end, get_resources(TU, TS))
+ end
+ end, [R || R <- get_resources(User, Server)]).
+
+roster_backend(Server) ->
+ Modules = gen_mod:loaded_modules(Server),
+ Mnesia = lists:member(mod_roster, Modules),
+ Odbc = lists:member(mod_roster_odbc, Modules),
+ if Mnesia -> mnesia;
+ true ->
+ if Odbc -> odbc;
+ true -> none
+ end
+ end.
+
+record_to_string(#roster{us = {User, _Server},
+ jid = JID,
+ name = Name,
+ subscription = Subscription,
+ ask = Ask,
+ askmessage = AskMessage}) ->
+ Username = ejabberd_odbc:escape(User),
+ SJID = ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))),
+ Nick = ejabberd_odbc:escape(Name),
+ SSubscription = case Subscription of
+ both -> "B";
+ to -> "T";
+ from -> "F";
+ none -> "N"
+ end,
+ SAsk = case Ask of
+ subscribe -> "S";
+ unsubscribe -> "U";
+ both -> "B";
+ out -> "O";
+ in -> "I";
+ none -> "N"
+ end,
+ SAskMessage = ejabberd_odbc:escape(AskMessage),
+ [Username, SJID, Nick, SSubscription, SAsk, SAskMessage, "N", "", "item"].
+
+groups_to_string(#roster{us = {User, _Server},
+ jid = JID,
+ groups = Groups}) ->
+ Username = ejabberd_odbc:escape(User),
+ SJID = ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))),
+ %% Empty groups do not need to be converted to string to be inserted in
+ %% the database
+ lists:foldl(fun([], Acc) -> Acc;
+ (Group, Acc) ->
+ String = ["'", Username, "',"
+ "'", SJID, "',"
+ "'", ejabberd_odbc:escape(Group), "'"],
+ [String|Acc]
+ end, [], Groups).
+
+%% Format roster items as a list of:
+%% [{struct, [{jid, "test@localhost"},{group, "Friends"},{nick, "Nicktest"}]}]
+format_roster([]) ->
+ [];
+format_roster(Items) ->
+ format_roster(Items, []).
+format_roster([], Structs) ->
+ Structs;
+format_roster([#roster{jid=JID, name=Nick, groups=Group,
+ subscription=Subs, ask=Ask}|Items], Structs) ->
+ {User,Server,_Resource} = JID,
+ Struct = {struct, [{jid,lists:flatten([User,"@",Server])},
+ {groups, extract_groups(Group)},
+ {nick, Nick},
+ {subscription, atom_to_list(Subs)},
+ {pending, atom_to_list(Ask)}
+ ]},
+ format_roster(Items, [Struct|Structs]).
+
+%% Format roster items as a list of:
+%% [{struct, [{jid, "test@localhost"}, {resource, "Messenger"}, {group, "Friends"},
+%% {nick, "Nicktest"},{show, "available"}, {status, "Currently at office"}]}]
+%% Note: If user is connected several times, only keep the resource with the
+%% highest non-negative priority
+format_roster_with_presence([]) ->
+ [];
+format_roster_with_presence(Items) ->
+ format_roster_with_presence(Items, []).
+format_roster_with_presence([], Structs) ->
+ Structs;
+format_roster_with_presence([#roster{jid=JID, name=Nick, groups=Group,
+ subscription=Subs, ask=Ask}|Items], Structs) ->
+ {User,Server,_R} = JID,
+ Presence = case Subs of
+ both -> get_presence(User, Server);
+ from -> get_presence(User, Server);
+ _Other -> {"", "unavailable", ""}
+ end,
+ {Resource, Show, Status} =
+ case Presence of
+ {_R, "invisible", _S} -> {"", "unavailable", ""};
+ _Status -> Presence
+ end,
+ Struct = {struct, [{jid,lists:flatten([User,"@",Server])},
+ {resource, Resource},
+ {group, extract_group(Group)},
+ {nick, Nick},
+ {subscription, atom_to_list(Subs)},
+ {pending, atom_to_list(Ask)},
+ {show, Show},
+ {status, Status}
+ ]},
+ format_roster_with_presence(Items, [Struct|Structs]).
+
+extract_group([]) -> [];
+%extract_group([Group|_Groups]) -> Group.
+extract_group(Groups) -> string:join(Groups, ";").
+
+extract_groups([]) -> [];
+%extract_groups([Group|_Groups]) -> Group.
+extract_groups(Groups) -> {array, Groups}.
+
+%% -----------------------------
+%% Internal session handling
+%% -----------------------------
+
+%% This is inspired from ejabberd_sm.erl
+get_presence(User, Server) ->
+ case get_sessions(User, Server) of
+ [] ->
+ {"", "unavailable", ""};
+ Ss ->
+ Session = hd(Ss),
+ if Session#session.priority >= 0 ->
+ Pid = element(2, Session#session.sid),
+ %{_User, _Resource, Show, Status} = rpc:call(node(Pid), ejabberd_c2s, get_presence, [Pid]),
+ {_User, Resource, Show, Status} = ejabberd_c2s:get_presence(Pid),
+ {Resource, Show, Status};
+ true ->
+ {"", "unavailable", ""}
+ end
+ end.
+
+get_resources(User, Server) ->
+ lists:map(fun(S) -> element(3, S#session.usr)
+ end, get_sessions(User, Server)).
+
+get_sessions(User, Server) ->
+ US = {jlib:nodeprep(User), jlib:nameprep(Server)},
+ Node = ejabberd_cluster:get_node(US),
+ case catch rpc:call(Node, mnesia, dirty_index_read,
+ [session, US, #session.us], 5000) of
+ Result when is_list(Result), Result /= [] ->
+ lists:reverse(lists:keysort(#session.priority, clean_session_list(Result)));
+ _ ->
+ []
+ end.
+
+clean_session_list(Ss) ->
+ clean_session_list(lists:keysort(#session.usr, Ss), []).
+
+clean_session_list([], Res) ->
+ Res;
+clean_session_list([S], Res) ->
+ [S | Res];
+clean_session_list([S1, S2 | Rest], Res) ->
+ if
+ S1#session.usr == S2#session.usr ->
+ if
+ S1#session.sid > S2#session.sid ->
+ clean_session_list([S1 | Rest], Res);
+ true ->
+ clean_session_list([S2 | Rest], Res)
+ end;
+ true ->
+ clean_session_list([S2 | Rest], [S1 | Res])
+ end.
+
+
+%% -----------------------------
+%% Internal function pattern
+%% -----------------------------
+
+user_action(User, Server, Fun, OK) ->
+ case ejabberd_auth:is_user_exists(User, Server) of
+ true ->
+ case catch Fun() of
+ OK ->
+ {false, {response, [0]}};
+ _ ->
+ {false, {response, [1]}}
+ end;
+ false ->
+ {false, {response, [404]}}
+ end.
diff --git a/src/odbc/ejabberd_odbc.erl b/src/odbc/ejabberd_odbc.erl
index 6ac5414ba..0c390b671 100644
--- a/src/odbc/ejabberd_odbc.erl
+++ b/src/odbc/ejabberd_odbc.erl
@@ -40,9 +40,10 @@
escape/1,
escape_like/1,
to_bool/1,
- encode_term/1,
- decode_term/1,
- keep_alive/1]).
+ keep_alive/1,
+ sql_query_on_all_connections/2,
+ encode_term/1,
+ decode_term/1]).
%% gen_fsm callbacks
-export([init/1,
@@ -100,6 +101,13 @@ start_link(Host, StartInterval) ->
sql_query(Host, Query) ->
sql_call(Host, {sql_query, Query}).
+%% Issue an SQL query on all the connections
+sql_query_on_all_connections(Host, Query) ->
+ F = fun(Pid) -> ?GEN_FSM:sync_send_event(Pid, {sql_cmd,
+ {sql_query, Query},
+ erlang:now()}, ?TRANSACTION_TIMEOUT) end,
+ lists:map(F, ejabberd_odbc_sup:get_pids(Host)).
+
%% SQL transaction based on a list of queries
%% This function automatically
sql_transaction(Host, Queries) when is_list(Queries) ->
@@ -437,13 +445,15 @@ sql_query_internal(Query) ->
State = get(?STATE_KEY),
Res = case State#state.db_type of
odbc ->
- odbc:sql_query(State#state.db_ref, Query);
+ odbc:sql_query(State#state.db_ref, Query, ?TRANSACTION_TIMEOUT - 1000);
pgsql ->
+ %% TODO: We need to propagate the TRANSACTION_TIMEOUT to pgsql driver, but no yet supported in driver.
+ %% See EJAB-1266
pgsql_to_odbc(pgsql:squery(State#state.db_ref, Query));
mysql ->
?DEBUG("MySQL, Send query~n~p~n", [Query]),
R = mysql_to_odbc(mysql_conn:fetch(State#state.db_ref,
- Query, self())),
+ Query, self(), ?TRANSACTION_TIMEOUT - 1000)),
%% ?INFO_MSG("MySQL, Received result~n~p~n", [R]),
R
end,
@@ -517,6 +527,7 @@ mysql_connect(Server, Port, DB, Username, Password) ->
case mysql_conn:start(Server, Port, Username, Password, DB, fun log/3) of
{ok, Ref} ->
mysql_conn:fetch(Ref, ["set names 'utf8';"], self()),
+ mysql_conn:fetch(Ref, ["SET SESSION query_cache_type=1;"], self()),
{ok, Ref};
Err ->
Err
diff --git a/src/odbc/mysql.sql b/src/odbc/mysql.sql
index c2611b0d2..33f34dc28 100644
--- a/src/odbc/mysql.sql
+++ b/src/odbc/mysql.sql
@@ -180,6 +180,19 @@ CREATE TABLE roster_version (
version text NOT NULL
) CHARACTER SET utf8;
+
+-- Needed if persistent room history is enabled (recent room history survive server restart)
+CREATE TABLE room_history (
+ room varchar(250) NOT NULL,
+ nick varchar(250) NOT NULL,
+ packet text,
+ have_subject boolean,
+ timestamp bigint,
+ size int
+) CHARACTER SET utf8;
+CREATE INDEX i_room_history USING BTREE ON room_history(room);
+
+
-- To update from 1.x:
-- ALTER TABLE rosterusers ADD COLUMN askmessage text AFTER ask;
-- UPDATE rosterusers SET askmessage = '';
diff --git a/src/odbc/odbc_queries.erl b/src/odbc/odbc_queries.erl
index 3ec3b1be5..c00cdd639 100644
--- a/src/odbc/odbc_queries.erl
+++ b/src/odbc/odbc_queries.erl
@@ -82,7 +82,10 @@
escape/1,
count_records_where/3,
get_roster_version/2,
- set_roster_version/2]).
+ set_roster_version/2,
+ add_roomhistory_sql/6,
+ clear_and_add_roomhistory/3,
+ load_roomhistory/2]).
%% We have only two compile time options for db queries:
%-define(generic, true).
@@ -261,6 +264,21 @@ users_number(LServer, [{prefix, Prefix}]) when is_list(Prefix) ->
users_number(LServer, []) ->
users_number(LServer).
+quote(Str) -> ["'", Str, "'"].
+add_roomhistory_sql(Room, Nick, Packet, HaveSubject, Timestamp, Size) ->
+ ["insert into room_history(room, nick, packet, have_subject, timestamp, size) "
+ "values (", quote(Room), ",", quote(Nick), ",", quote(Packet), ",", HaveSubject, ",", Timestamp,
+ ",", Size, ");"].
+
+clear_and_add_roomhistory(LServer, Room, Queries) ->
+ Q = ["delete from room_history where room = '", Room, "';"],
+ ejabberd_odbc:sql_transaction(LServer, [Q|Queries]).
+
+load_roomhistory(LServer, Room) ->
+ Q = ["select nick, packet, have_subject, timestamp, size from room_history where room = '", Room,
+ "' order by timestamp ;"],
+ ejabberd_odbc:sql_query(LServer, Q).
+
add_spool_sql(Username, XML) ->
["insert into spool(username, xml) "
diff --git a/src/p1_fsm.erl b/src/p1_fsm.erl
index 0f04429ad..c3a7301e9 100644
--- a/src/p1_fsm.erl
+++ b/src/p1_fsm.erl
@@ -395,7 +395,8 @@ loop(Parent, Name, StateName, StateData, Mod, Time, Debug,
{process_limit, Limit} ->
Reason = {process_limit, Limit},
Msg = {'EXIT', Parent, {error, {process_limit, Limit}}},
- terminate(Reason, Name, Msg, Mod, StateName, StateData, Debug)
+ terminate(Reason, Name, Msg, Mod, StateName, StateData, Debug,
+ queue:new())
end,
process_message(Parent, Name, StateName, StateData,
Mod, Time, Debug, Limits, Queue, QueueLen).
@@ -451,7 +452,8 @@ decode_msg(Msg,Parent, Name, StateName, StateData, Mod, Time, Debug,
[Name, StateName, StateData,
Mod, Time, Limits, Queue, QueueLen], Hib);
{'EXIT', Parent, Reason} ->
- terminate(Reason, Name, Msg, Mod, StateName, StateData, Debug);
+ terminate(Reason, Name, Msg, Mod, StateName, StateData, Debug,
+ Queue);
_Msg when Debug == [] ->
handle_msg(Msg, Parent, Name, StateName, StateData,
Mod, Time, Limits, Queue, QueueLen);
@@ -473,8 +475,9 @@ system_continue(Parent, Debug, [Name, StateName, StateData,
-spec system_terminate(term(), _, _, [term(),...]) -> no_return().
system_terminate(Reason, _Parent, Debug,
- [Name, StateName, StateData, Mod, _Time, _Limits]) ->
- terminate(Reason, Name, [], Mod, StateName, StateData, Debug).
+ [Name, StateName, StateData, Mod, _Time,
+ _Limits, Queue, _QueueLen]) ->
+ terminate(Reason, Name, [], Mod, StateName, StateData, Debug, Queue).
system_code_change([Name, StateName, StateData, Mod, Time,
Limits, Queue, QueueLen],
@@ -527,8 +530,8 @@ relay_messages(MRef, TRef, Clone, Queue) ->
relay_messages(MRef, TRef, Clone) ->
receive
- {'DOWN', MRef, process, Clone, Reason} ->
- Reason;
+ {'DOWN', MRef, process, Clone, _Reason} ->
+ normal;
{'EXIT', _Parent, _Reason} ->
{migrated, Clone};
{timeout, TRef, timeout} ->
@@ -557,34 +560,44 @@ handle_msg(Msg, Parent, Name, StateName, StateData, Mod, _Time,
loop(Parent, Name, NStateName, NStateData, Mod, Time1, [],
Limits, Queue, QueueLen);
{migrate, NStateData, {Node, M, F, A}, Time1} ->
- Reason = case catch rpc:call(Node, M, F, A, 5000) of
- {badrpc, _} = Err ->
- {migration_error, Err};
- {'EXIT', _} = Err ->
- {migration_error, Err};
- {error, _} = Err ->
- {migration_error, Err};
+ RPCTimeout = if Time1 == 0 ->
+ %% We don't care about a delay,
+ %% so we set it one minute
+ 60000;
+ true ->
+ Time1
+ end,
+ Now = now(),
+ Reason = case catch rpc_call(Node, M, F, A, RPCTimeout) of
{ok, Clone} ->
process_flag(trap_exit, true),
MRef = erlang:monitor(process, Clone),
- TRef = erlang:start_timer(Time1, self(), timeout),
+ NowDiff = timer:now_diff(now(), Now) div 1000,
+ TimeLeft = lists:max([Time1 - NowDiff, 0]),
+ TRef = erlang:start_timer(TimeLeft, self(), timeout),
relay_messages(MRef, TRef, Clone, Queue);
- Reply ->
- {migration_error, {bad_reply, Reply}}
+ _ ->
+ normal
end,
- terminate(Reason, Name, Msg, Mod, StateName, NStateData, []);
+ Queue1 =
+ case Reason of
+ normal -> Queue;
+ _ -> queue:new()
+ end,
+ terminate(Reason, Name, Msg, Mod, StateName, NStateData, [],
+ Queue1);
{stop, Reason, NStateData} ->
- terminate(Reason, Name, Msg, Mod, StateName, NStateData, []);
+ terminate(Reason, Name, Msg, Mod, StateName, NStateData, [], Queue);
{stop, Reason, Reply, NStateData} when From =/= undefined ->
{'EXIT', R} = (catch terminate(Reason, Name, Msg, Mod,
- StateName, NStateData, [])),
+ StateName, NStateData, [], Queue)),
reply(From, Reply),
exit(R);
{'EXIT', What} ->
- terminate(What, Name, Msg, Mod, StateName, StateData, []);
+ terminate(What, Name, Msg, Mod, StateName, StateData, [], Queue);
Reply ->
terminate({bad_return_value, Reply},
- Name, Msg, Mod, StateName, StateData, [])
+ Name, Msg, Mod, StateName, StateData, [], Queue)
end.
handle_msg(Msg, Parent, Name, StateName, StateData,
@@ -610,34 +623,46 @@ handle_msg(Msg, Parent, Name, StateName, StateData,
loop(Parent, Name, NStateName, NStateData,
Mod, Time1, Debug1, Limits, Queue, QueueLen);
{migrate, NStateData, {Node, M, F, A}, Time1} ->
- Reason = case catch rpc:call(Node, M, F, A, Time1) of
- {badrpc, R} ->
- {migration_error, R};
- {'EXIT', R} ->
- {migration_error, R};
- {error, R} ->
- {migration_error, R};
+ RPCTimeout = if Time1 == 0 ->
+ %% We don't care about a delay,
+ %% so we set it one minute
+ 60000;
+ true ->
+ Time1
+ end,
+ Now = now(),
+ Reason = case catch rpc_call(Node, M, F, A, RPCTimeout) of
{ok, Clone} ->
process_flag(trap_exit, true),
MRef = erlang:monitor(process, Clone),
- TRef = erlang:start_timer(Time1, self(), timeout),
+ NowDiff = timer:now_diff(now(), Now) div 1000,
+ TimeLeft = lists:max([Time1 - NowDiff, 0]),
+ TRef = erlang:start_timer(TimeLeft, self(), timeout),
relay_messages(MRef, TRef, Clone, Queue);
- Reply ->
- {migration_error, {bad_reply, Reply}}
+ _ ->
+ normal
end,
- terminate(Reason, Name, Msg, Mod, StateName, NStateData, Debug);
+ Queue1 =
+ case Reason of
+ normal -> Queue;
+ _ -> queue:new()
+ end,
+ terminate(Reason, Name, Msg, Mod, StateName, NStateData, Debug,
+ Queue1);
{stop, Reason, NStateData} ->
- terminate(Reason, Name, Msg, Mod, StateName, NStateData, Debug);
+ terminate(Reason, Name, Msg, Mod, StateName, NStateData, Debug,
+ Queue);
{stop, Reason, Reply, NStateData} when From =/= undefined ->
{'EXIT', R} = (catch terminate(Reason, Name, Msg, Mod,
- StateName, NStateData, Debug)),
+ StateName, NStateData, Debug,
+ Queue)),
reply(Name, From, Reply, Debug, StateName),
exit(R);
{'EXIT', What} ->
- terminate(What, Name, Msg, Mod, StateName, StateData, Debug);
+ terminate(What, Name, Msg, Mod, StateName, StateData, Debug, Queue);
Reply ->
terminate({bad_return_value, Reply},
- Name, Msg, Mod, StateName, StateData, Debug)
+ Name, Msg, Mod, StateName, StateData, Debug, Queue)
end.
dispatch({'$gen_event', Event}, Mod, StateName, StateData) ->
@@ -673,9 +698,10 @@ reply(Name, {To, Tag}, Reply, Debug, StateName) ->
%%% Terminate the server.
%%% ---------------------------------------------------
--spec terminate(term(), _, _, atom(), _, _, _) -> no_return().
-
-terminate(Reason, Name, Msg, Mod, StateName, StateData, Debug) ->
+terminate(Reason, Name, Msg, Mod, StateName, StateData, Debug, Queue) ->
+ lists:foreach(
+ fun(Message) -> self() ! Message end,
+ queue:to_list(Queue)),
case catch Mod:terminate(Reason, StateName, StateData) of
{'EXIT', R} ->
error_info(Mod, R, Name, Msg, StateName, StateData, Debug),
@@ -758,7 +784,8 @@ get_msg(Msg) -> Msg.
%% Status information
%%-----------------------------------------------------------------
format_status(Opt, StatusData) ->
- [PDict, SysState, Parent, Debug, [Name, StateName, StateData, Mod, _Time]] =
+ [PDict, SysState, Parent, Debug,
+ [Name, StateName, StateData, Mod, _Time, _Limits, _Queue, _QueueLen]] =
StatusData,
NameTag = if is_pid(Name) ->
pid_to_list(Name);
@@ -812,3 +839,34 @@ message_queue_len(#limits{max_queue = MaxQueue}, QueueLen) ->
_ ->
ok
end.
+
+rpc_call(Node, Mod, Fun, Args, Timeout) ->
+ Ref = make_ref(),
+ Caller = self(),
+ F = fun() ->
+ group_leader(whereis(user), self()),
+ case catch apply(Mod, Fun, Args) of
+ {'EXIT', _} = Err ->
+ Caller ! {Ref, {badrpc, Err}};
+ Result ->
+ Caller ! {Ref, Result}
+ end
+ end,
+ Pid = spawn(Node, F),
+ MRef = erlang:monitor(process, Pid),
+ receive
+ {Ref, Result} ->
+ erlang:demonitor(MRef, [flush]),
+ Result;
+ {'DOWN', MRef, _, _, noconnection = Reason} ->
+ {badrpc, Reason}
+ after Timeout ->
+ erlang:demonitor(MRef, [flush]),
+ catch exit(Pid, kill),
+ receive
+ {Ref, Result} ->
+ Result
+ after 0 ->
+ {badrpc, timeout}
+ end
+ end.
diff --git a/src/sha.erl b/src/sha.erl
index 87948bc73..ff0c2ab61 100644
--- a/src/sha.erl
+++ b/src/sha.erl
@@ -1,7 +1,7 @@
%%%----------------------------------------------------------------------
%%% File : sha.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
-%%% Purpose :
+%%% Purpose :
%%% Created : 20 Dec 2002 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
@@ -28,7 +28,7 @@
-author('alexey@process-one.net').
-export([start/0, sha/1, sha1/1, sha224/1, sha256/1, sha384/1,
- sha512/1]).
+ sha512/1, to_hexlist/1]).
-ifdef(HAVE_MD2).
-export([md2/1]).
@@ -61,6 +61,9 @@ digit_to_xchar(D) ->
sha(Text) ->
Bin = crypto:sha(Text),
+ to_hexlist(Bin).
+
+to_hexlist(Bin) ->
lists:reverse(ints_to_rxstr(binary_to_list(Bin), [])).
ints_to_rxstr([], Res) ->
diff --git a/src/tcp_serv.erl b/src/tcp_serv.erl
new file mode 100644
index 000000000..51e00dcc3
--- /dev/null
+++ b/src/tcp_serv.erl
@@ -0,0 +1,156 @@
+%% Copyright (C) 2003 Joakim Grebenö <jocke@gleipnir.com>.
+%% All rights reserved.
+%%
+%% Redistribution and use in source and binary forms, with or without
+%% modification, are permitted provided that the following conditions
+%% are met:
+%%
+%% 1. Redistributions of source code must retain the above copyright
+%% notice, this list of conditions and the following disclaimer.
+%% 2. 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.
+%%
+%% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
+
+-module(tcp_serv).
+-vsn("1.13").
+-author('jocke@gleipnir.com').
+-export([start_link/1, start_link/2, stop/1, stop/2]).
+-export([init/2, start_session/3]).
+-export([system_continue/3, system_terminate/4]).
+
+-include("log.hrl").
+
+-record(state, {
+ %% int()
+ max_sessions,
+ %% {M, F, A}
+ %% M = F = atom()
+ %% A = [term()]
+ session_handler,
+ %% [pid()]
+ session_list,
+ %% socket()
+ listen_socket,
+ %% pid()
+ parent,
+ %% term()
+ debug_info
+ }).
+
+%% Exported: start_link/{1,2}
+
+start_link(Args) -> start_link(Args, 60000).
+
+start_link(Args, Timeout) ->
+ Pid = proc_lib:spawn_link(?MODULE, init, [self(), Args]),
+ receive
+ {Pid, started} -> {ok, Pid};
+ {Pid, Reason} -> {error, Reason}
+ after Timeout -> {error, timeout}
+ end.
+
+%% Exported: stop/{1,2}
+
+stop(Pid) -> stop(Pid, 15000).
+
+stop(Pid, Timeout) ->
+ Pid ! {self(), stop},
+ receive
+ {Pid, Reply} -> Reply
+ after
+ Timeout -> {error, timeout}
+ end.
+
+%% Exported: init/2
+
+init(Parent, [Port, MaxSessions, OptionList, SessionHandler]) ->
+ process_flag(trap_exit, true),
+ case gen_tcp:listen(Port, OptionList) of
+ {ok, ListenSocket} ->
+ self() ! start_session,
+ Parent ! {self(), started},
+ loop(#state{max_sessions = MaxSessions,
+ session_handler = SessionHandler,
+ session_list = [],
+ listen_socket = ListenSocket,
+ parent = Parent});
+ Reason -> Parent ! {self(), {not_started, Reason}}
+ end.
+
+loop(#state{session_list = SessionList, listen_socket = ListenSocket,
+ parent = Parent} = State) ->
+ receive
+ {From, stop} ->
+ cleanup(State),
+ From ! {self(), ok};
+ start_session when length(SessionList) > State#state.max_sessions ->
+ timer:sleep(5000),
+ self() ! start_session,
+ loop(State);
+ start_session ->
+ A = [self(), State#state.session_handler, ListenSocket],
+ Pid = proc_lib:spawn_link(?MODULE, start_session, A),
+ loop(State#state{session_list = [Pid|SessionList]});
+ {'EXIT', Parent, Reason} ->
+ cleanup(State),
+ exit(Reason);
+ {'EXIT', Pid, Reason} ->
+ case lists:member(Pid, SessionList) of
+ true ->
+ PurgedSessionList = lists:delete(Pid, SessionList),
+ loop(State#state{session_list = PurgedSessionList});
+ false ->
+ ?ERROR_LOG({ignoring, {'EXIT', Pid, Reason}}),
+ loop(State)
+ end;
+ {system, From, Request} ->
+ sys:handle_system_msg(Request, From, Parent, ?MODULE,
+ State#state.debug_info, State);
+ UnknownMessage ->
+ ?ERROR_LOG({unknown_message, UnknownMessage}),
+ loop(State)
+ end.
+
+cleanup(State) -> gen_tcp:close(State#state.listen_socket).
+
+%% Exported: start_seesion/3
+
+start_session(Parent, {M, F, A}, ListenSocket) ->
+ case gen_tcp:accept(ListenSocket) of
+ {ok, Socket} ->
+ Parent ! start_session,
+ case apply(M, F, [Socket|A]) of
+ ok -> gen_tcp:close(Socket);
+ {error, closed} -> ok;
+ {error, Reason} ->
+ ?ERROR_LOG({M, F, Reason}),
+ gen_tcp:close(Socket)
+ end;
+ {error, Reason} ->
+ timer:sleep(5000),
+ Parent ! start_session
+ end.
+
+%% Exported: system_continue/3
+
+system_continue(Parent, DebugInfo, State) ->
+ loop(State#state{parent = Parent, debug_info = DebugInfo}).
+
+%% Exported: system_terminate/3
+
+system_terminate(Reason, Parent, DebugInfo, State) ->
+ cleanup(State),
+ exit(Reason).
diff --git a/src/tls/tls_drv.c b/src/tls/tls_drv.c
index 6dbdccbe9..e45b81679 100644
--- a/src/tls/tls_drv.c
+++ b/src/tls/tls_drv.c
@@ -386,7 +386,8 @@ static ErlDrvSSizeT tls_drv_control(ErlDrvData handle,
SSL_set_bio(d->ssl, d->bio_read, d->bio_write);
if (command == SET_CERTIFICATE_FILE_ACCEPT) {
- SSL_set_options(d->ssl, SSL_OP_NO_TICKET);
+ SSL_set_options(d->ssl, SSL_OP_NO_SSLv2|SSL_OP_NO_TICKET|SSL_OP_ALL);
+
SSL_set_accept_state(d->ssl);
} else {
SSL_set_options(d->ssl, SSL_OP_NO_SSLv2|SSL_OP_NO_TICKET);
@@ -400,6 +401,7 @@ static ErlDrvSSizeT tls_drv_control(ErlDrvData handle,
break;
case SET_DECRYPTED_OUTPUT:
die_unless(d->ssl, "SSL not initialized");
+
res = SSL_write(d->ssl, buf, len);
if (res <= 0)
{
diff --git a/src/web/bosh.hrl b/src/web/bosh.hrl
new file mode 100644
index 000000000..d47bd7c94
--- /dev/null
+++ b/src/web/bosh.hrl
@@ -0,0 +1,34 @@
+%%%----------------------------------------------------------------------
+%%%
+%%% ejabberd, Copyright (C) 2002-2011 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+-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, POST, 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]).
+
+-define(PROCNAME, ejabberd_mod_bosh).
diff --git a/src/web/ejabberd_bosh.erl b/src/web/ejabberd_bosh.erl
new file mode 100644
index 000000000..36f73abfd
--- /dev/null
+++ b/src/web/ejabberd_bosh.erl
@@ -0,0 +1,994 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_bosh.erl
+%%% Author : Evgeniy Khramtsov <ekhramtsov@process-one.net>
+%%% Purpose : Manage BOSH sockets
+%%% Created : 20 Jul 2011 by Evgeniy Khramtsov <ekhramtsov@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2011 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+-module(ejabberd_bosh).
+
+-define(GEN_FSM, p1_fsm).
+
+-behaviour(?GEN_FSM).
+
+%% API
+-export([start/2, start/3, start_link/3]).
+-export([send_xml/2, setopts/2, controlling_process/2, migrate/3,
+ custom_receiver/1, become_controller/2, reset_stream/1,
+ change_shaper/2, monitor/1, close/1, sockname/1,
+ peername/1, process_request/2, send/2, change_controller/2]).
+
+%% gen_fsm callbacks
+-export([init/1, wait_for_session/2, wait_for_session/3,
+ active/2, active/3, handle_event/3, print_state/1,
+ handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("ejabberd_http.hrl").
+-include("bosh.hrl").
+
+%%-define(DBGFSM, true).
+-ifdef(DBGFSM).
+-define(FSMOPTS, [{debug, [trace]}]).
+-else.
+-define(FSMOPTS, []).
+-endif.
+
+-define(BOSH_VERSION, "1.10").
+-define(NS_CLIENT, "jabber:client").
+-define(NS_BOSH, "urn:xmpp:xbosh").
+-define(NS_HTTP_BIND, "http://jabber.org/protocol/httpbind").
+
+-define(DEFAULT_MAXPAUSE, 120). %% secs
+-define(DEFAULT_WAIT, 300). %% secs
+-define(DEFAULT_HOLD, 1). %% num
+-define(DEFAULT_POLLING, 2). %% secs
+-define(DEFAULT_INACTIVITY, 30). %% secs
+
+-define(MAX_SHAPED_REQUESTS_QUEUE_LEN, 1000).
+-define(SEND_TIMEOUT, 15000). %% millisecs
+
+-record(state, {host,
+ sid,
+ el_ibuf,
+ el_obuf,
+ shaper_state,
+ c2s_pid,
+ xmpp_ver,
+ inactivity_timer,
+ wait_timer,
+ wait_timeout = ?DEFAULT_WAIT,
+ inactivity_timeout,
+ prev_rid,
+ prev_key,
+ prev_poll,
+ max_concat = unlimited,
+ responses = gb_trees:empty(),
+ receivers = gb_trees:empty(),
+ shaped_receivers = queue:new(),
+ ip,
+ max_requests}).
+
+-record(body, {http_reason = "", %% Using HTTP reason phrase is
+ %% a hack, but we need a clue why
+ %% a connection gets terminated:
+ %% 'condition' attribute is not enough
+ attrs = [],
+ els = [],
+ size = 0}).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+%% TODO: If compile with no supervisor option, start the session without
+%% supervisor
+start(#body{attrs = Attrs} = Body, IP, SID) ->
+ XMPPDomain = get_attr('to', Attrs),
+ Node = ejabberd_cluster:get_node(SID),
+ SupervisorProc = {gen_mod:get_module_proc(XMPPDomain, ?PROCNAME), Node},
+ case catch supervisor:start_child(SupervisorProc, [Body, IP, SID]) of
+ {ok, Pid} ->
+ {ok, Pid};
+ {'EXIT', {noproc, _}} ->
+ check_bosh_module(XMPPDomain),
+ {error, module_not_loaded};
+ Err ->
+ ?ERROR_MSG("Failed to start BOSH session: ~p", [Err]),
+ {error, Err}
+ end.
+
+start(StateName, State) ->
+ ?GEN_FSM:start_link(?MODULE, [StateName, State], ?FSMOPTS).
+
+start_link(Body, IP, SID) ->
+ ?GEN_FSM:start_link(?MODULE, [Body, IP, SID], ?FSMOPTS).
+
+send({http_bind, _FsmRef, _IP}, _Packet) ->
+ {error, badarg}.
+
+send_xml({http_bind, FsmRef, _IP}, Packet) ->
+ case catch ?GEN_FSM:sync_send_all_state_event(
+ FsmRef, {send_xml, Packet}, ?SEND_TIMEOUT) of
+ {'EXIT', {timeout, _}} ->
+ {error, timeout};
+ {'EXIT', _} ->
+ {error, einval};
+ Res ->
+ Res
+ end.
+
+setopts({http_bind, FsmRef, _IP}, Opts) ->
+ case lists:member({active, once}, Opts) of
+ true ->
+ ?GEN_FSM:send_all_state_event(FsmRef, {activate, self()});
+ _ ->
+ case lists:member({active, false}, Opts) of
+ true ->
+ case catch ?GEN_FSM:sync_send_all_state_event(
+ FsmRef, deactivate_socket) of
+ {'EXIT', _} ->
+ {error, einval};
+ Res ->
+ Res
+ end;
+ _ ->
+ ok
+ end
+ end.
+
+controlling_process(_Socket, _Pid) ->
+ ok.
+
+custom_receiver({http_bind, FsmRef, _IP}) ->
+ {receiver, ?MODULE, FsmRef}.
+
+become_controller(FsmRef, C2SPid) ->
+ ?GEN_FSM:send_all_state_event(FsmRef, {become_controller, C2SPid}).
+
+change_controller({http_bind, FsmRef, _IP}, C2SPid) ->
+ become_controller(FsmRef, C2SPid).
+
+reset_stream({http_bind, _FsmRef, _IP}) ->
+ ok.
+
+change_shaper({http_bind, FsmRef, _IP}, Shaper) ->
+ ?GEN_FSM:send_all_state_event(FsmRef, {change_shaper, Shaper}).
+
+monitor({http_bind, FsmRef, _IP}) ->
+ erlang:monitor(process, FsmRef).
+
+close({http_bind, FsmRef, _IP}) ->
+ catch ?GEN_FSM:sync_send_all_state_event(FsmRef, close).
+
+sockname(_Socket) ->
+ {ok, {{0,0,0,0}, 0}}.
+
+peername({http_bind, _FsmRef, IP}) ->
+ {ok, IP}.
+
+migrate(FsmRef, Node, After) ->
+ erlang:send_after(After, FsmRef, {migrate, Node}).
+
+process_request(Data, IP) ->
+ Opts1 = ejabberd_c2s_config:get_c2s_limits(),
+ Opts = [{xml_socket, true} | Opts1],
+ MaxStanzaSize =
+ case lists:keysearch(max_stanza_size, 1, Opts) of
+ {value, {_, Size}} -> Size;
+ _ -> infinity
+ end,
+ PayloadSize = iolist_size(Data),
+ if PayloadSize > MaxStanzaSize ->
+ http_error(403, "Request Too Large");
+ true ->
+ case decode_body(Data, PayloadSize) of
+ {ok, #body{attrs = Attrs} = Body} ->
+ SID = get_attr('sid', Attrs),
+ To = get_attr('to', Attrs),
+ if SID == "", To == "" ->
+ %% Initial request which lacks "to" attribute
+ bosh_response(
+ #body{http_reason = "Missing 'to' attribute",
+ attrs = [{type, "terminate"},
+ {condition, "improper-addressing"}]});
+ SID == "" ->
+ %% Initial request
+ case start(Body, IP, make_sid()) of
+ {ok, Pid} ->
+ process_request(Pid, Body, IP);
+ _Err ->
+ bosh_response(
+ #body{http_reason =
+ "Failed to start BOSH session",
+ attrs = [{type, "terminate"},
+ {condition,
+ "internal-server-error"}]})
+ end;
+ true ->
+ case mod_bosh:find_session(SID) of
+ {ok, Pid} ->
+ process_request(Pid, Body, IP);
+ error ->
+ bosh_response(
+ #body{http_reason = "Session ID mismatch",
+ attrs = [{type, "terminate"},
+ {condition,
+ "item-not-found"}]})
+ end
+ end;
+ {error, Reason} ->
+ http_error(400, Reason)
+ end
+ end.
+
+process_request(Pid, Req, _IP) ->
+ case catch ?GEN_FSM:sync_send_event(Pid, Req, infinity) of
+ #body{} = Resp ->
+ bosh_response(Resp);
+ {'EXIT', {Reason, _}} when Reason == noproc; Reason == normal ->
+ bosh_response(#body{http_reason = "BOSH session not found",
+ attrs = [{type, "terminate"},
+ {condition,
+ "item-not-found"}]});
+ {'EXIT', _} ->
+ bosh_response(#body{http_reason = "Unexpected error",
+ attrs = [{type, "terminate"},
+ {condition, "internal-server-error"}]})
+ end.
+
+%%%===================================================================
+%%% gen_fsm callbacks
+%%%===================================================================
+init([#body{attrs = Attrs}, IP, SID]) ->
+ %% Read c2s options from the first ejabberd_c2s configuration in
+ %% the config file listen section
+ %% TODO: We should have different access and shaper values for
+ %% each connector. The default behaviour should be however to use
+ %% the default c2s restrictions if not defined for the current
+ %% connector.
+ Opts1 = ejabberd_c2s_config:get_c2s_limits(),
+ Opts2 = [{xml_socket, true} | Opts1],
+ Shaper = none,
+ ShaperState = shaper:new(Shaper),
+ Socket = make_socket(self(), IP),
+ XMPPVer = get_attr('xmpp:version', Attrs),
+ XMPPDomain = get_attr('to', Attrs),
+ {InBuf, Opts} = case gen_mod:get_module_opt(XMPPDomain, mod_bosh,
+ prebind, false) of
+ true ->
+ JID = make_random_jid(XMPPDomain),
+ {buf_new(), [{jid, JID} | Opts2]};
+ false ->
+ {buf_in([make_xmlstreamstart(XMPPDomain, XMPPVer)],
+ buf_new()),
+ Opts2}
+ end,
+ ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket, Opts),
+ Inactivity = gen_mod:get_module_opt(XMPPDomain, mod_bosh,
+ max_inactivity, ?DEFAULT_INACTIVITY),
+ MaxConcat = gen_mod:get_module_opt(XMPPDomain, mod_bosh,
+ max_concat, unlimited),
+ State = #state{host = XMPPDomain,
+ sid = SID,
+ ip = IP,
+ xmpp_ver = XMPPVer,
+ el_ibuf = InBuf,
+ max_concat = MaxConcat,
+ el_obuf = buf_new(),
+ inactivity_timeout = Inactivity,
+ shaper_state = ShaperState},
+ NewState = restart_inactivity_timer(State),
+ mod_bosh:open_session(SID, self()),
+ {ok, wait_for_session, NewState};
+init([StateName, State]) ->
+ mod_bosh:open_session(State#state.sid, self()),
+ case State#state.c2s_pid of
+ C2SPid when is_pid(C2SPid) ->
+ NewSocket = make_socket(self(), State#state.ip),
+ C2SPid ! {change_socket, NewSocket},
+ NewState = restart_inactivity_timer(State),
+ {ok, StateName, NewState};
+ _ ->
+ %% TODO: it seems like we're losing the connection :-/
+ {stop, normal}
+ end.
+
+wait_for_session(_Event, State) ->
+ ?ERROR_MSG("unexpected event in 'wait_for_session': ~p", [_Event]),
+ {next_state, wait_for_session, State}.
+
+wait_for_session(#body{attrs = Attrs} = Req, From, State) ->
+ RID = get_attr('rid', Attrs),
+ ?DEBUG("got request:~n"
+ "** RequestID: ~p~n"
+ "** Request: ~p~n"
+ "** From: ~p~n"
+ "** State: ~p",
+ [RID, Req, From, State]),
+ Wait = min(get_attr('wait', Attrs, undefined), ?DEFAULT_WAIT),
+ Hold = min(get_attr('hold', Attrs, undefined), ?DEFAULT_HOLD),
+ NewKey = get_attr('newkey', Attrs),
+ Type = get_attr('type', Attrs),
+ Requests = Hold + 1,
+ {PollTime, Polling} = if Wait == 0, Hold == 0 ->
+ {now(), [{polling, ?DEFAULT_POLLING}]};
+ true ->
+ {undefined, []}
+ end,
+ MaxPause = gen_mod:get_module_opt(State#state.host, mod_bosh,
+ max_pause, ?DEFAULT_MAXPAUSE),
+ Resp = #body{attrs = [{sid, State#state.sid},
+ {wait, Wait},
+ {ver, ?BOSH_VERSION},
+ {polling, ?DEFAULT_POLLING},
+ {inactivity, State#state.inactivity_timeout},
+ {hold, Hold},
+ {'xmpp:restartlogic', true},
+ {requests, Requests},
+ {secure, true},
+ {maxpause, MaxPause},
+ {'xmlns:xmpp', ?NS_BOSH},
+ {'xmlns:stream', ?NS_STREAM},
+ {from, State#state.host}|Polling]},
+ {ShaperState, _} = shaper:update(State#state.shaper_state, Req#body.size),
+ State1 = State#state{wait_timeout = Wait,
+ prev_rid = RID,
+ prev_key = NewKey,
+ prev_poll = PollTime,
+ shaper_state = ShaperState,
+ max_requests = Requests},
+ Els = maybe_add_xmlstreamend(Req#body.els, Type),
+ State2 = route_els(State1, Els),
+ {State3, RespEls} = get_response_els(State2),
+ State4 = stop_inactivity_timer(State3),
+ case RespEls of
+ [] ->
+ State5 = restart_wait_timer(State4),
+ Receivers = gb_trees:insert(RID, {From, Resp},
+ State5#state.receivers),
+ {next_state, active, State5#state{receivers = Receivers}};
+ _ ->
+ reply_next_state(State4, Resp#body{els = RespEls}, RID, From)
+ end;
+wait_for_session(_Event, _From, State) ->
+ ?ERROR_MSG("unexpected sync event in 'wait_for_session': ~p", [_Event]),
+ {reply, {error, badarg}, wait_for_session, State}.
+
+active({#body{} = Body, From}, State) ->
+ active1(Body, From, State);
+active(_Event, State) ->
+ ?ERROR_MSG("unexpected event in 'active': ~p", [_Event]),
+ {next_state, active, State}.
+
+active(#body{attrs = Attrs, size = Size} = Req, From, State) ->
+ ?DEBUG("got request:~n"
+ "** Request: ~p~n"
+ "** From: ~p~n"
+ "** State: ~p",
+ [Req, From, State]),
+ {ShaperState, Pause} = shaper:update(State#state.shaper_state, Size),
+ State1 = State#state{shaper_state = ShaperState},
+ if Pause > 0 ->
+ QLen = queue:len(State1#state.shaped_receivers),
+ if QLen < ?MAX_SHAPED_REQUESTS_QUEUE_LEN ->
+ TRef = start_shaper_timer(Pause),
+ Q = queue:in({TRef, From, Req}, State1#state.shaped_receivers),
+ State2 = stop_inactivity_timer(State1),
+ {next_state, active, State2#state{shaped_receivers = Q}};
+ true ->
+ RID = get_attr('rid', Attrs),
+ reply_stop(State1,
+ #body{http_reason = "Too many requests",
+ attrs = [{"type", "terminate"},
+ {"condition", "policy-violation"}]},
+ From, RID)
+ end;
+ true ->
+ active1(Req, From, State1)
+ end;
+active(_Event, _From, State) ->
+ ?ERROR_MSG("unexpected sync event in 'active': ~p", [_Event]),
+ {reply, {error, badarg}, active, State}.
+
+active1(#body{attrs = Attrs} = Req, From, State) ->
+ RID = get_attr('rid', Attrs),
+ Key = get_attr('key', Attrs),
+ IsValidKey = is_valid_key(State#state.prev_key, Key),
+ IsOveractivity = is_overactivity(State#state.prev_poll),
+ Type = get_attr('type', Attrs),
+ if RID > State#state.prev_rid + State#state.max_requests ->
+ reply_stop(State,
+ #body{http_reason = "Request ID is out of range",
+ attrs = [{"type", "terminate"},
+ {"condition", "item-not-found"}]},
+ From, RID);
+ RID > State#state.prev_rid + 1 ->
+ State1 = restart_inactivity_timer(State),
+ %% TODO: gb_trees:insert/3 may raise an exception
+ Receivers = gb_trees:insert(RID, {From, Req},
+ State1#state.receivers),
+ {next_state, active, State1#state{receivers = Receivers}};
+ RID =< State#state.prev_rid ->
+ %% TODO: do we need to check 'key' here? It seems so...
+ case gb_trees:lookup(RID, State#state.responses) of
+ {value, PrevBody} ->
+ {next_state, active, do_reply(State, From, PrevBody, RID)};
+ none ->
+ reply_stop(State,
+ #body{http_reason = "Request ID is out of range",
+ attrs = [{"type", "terminate"},
+ {"condition", "item-not-found"}]},
+ From, RID)
+ end;
+ not IsValidKey ->
+ reply_stop(State,
+ #body{http_reason = "Session key mismatch",
+ attrs = [{"type", "terminate"},
+ {"condition", "item-not-found"}]},
+ From, RID);
+ IsOveractivity ->
+ reply_stop(State,
+ #body{http_reason = "Too many requests",
+ attrs = [{"type", "terminate"},
+ {"condition", "policy-violation"}]},
+ From, RID);
+ true ->
+ State1 = stop_inactivity_timer(State),
+ State2 = stop_wait_timer(State1),
+ Els = case get_attr('xmpp:restart', Attrs, false) of
+ true ->
+ XMPPDomain = get_attr('to', Attrs,
+ State#state.host),
+ XMPPVer = get_attr('xmpp:version', Attrs,
+ State#state.xmpp_ver),
+ [make_xmlstreamstart(XMPPDomain, XMPPVer)];
+ false ->
+ Req#body.els
+ end,
+ State3 = route_els(State2, maybe_add_xmlstreamend(Els, Type)),
+ {State4, RespEls} = get_response_els(State3),
+ NewKey = get_attr('newkey', Attrs, Key),
+ Pause = get_attr('pause', Attrs, undefined),
+ NewPoll = case State#state.prev_poll of
+ undefined -> undefined;
+ _ -> now()
+ end,
+ State5 = State4#state{prev_poll = NewPoll,
+ prev_key = NewKey},
+ if Type == "terminate" ->
+ reply_stop(State5, #body{http_reason = "Session close",
+ attrs = [{"type", "terminate"}],
+ els = RespEls}, From, RID);
+ Pause /= undefined ->
+ State6 = drop_holding_receiver(State5),
+ State7 = restart_inactivity_timer(State6, Pause),
+ InBuf = buf_in(RespEls, State7#state.el_ibuf),
+ {next_state, active,
+ State7#state{prev_rid = RID,
+ el_ibuf = InBuf}};
+ RespEls == [] ->
+ State6 = drop_holding_receiver(State5),
+ State7 = restart_wait_timer(State6),
+ %% TODO: gb_trees:insert/3 may raise an exception
+ Receivers = gb_trees:insert(RID, {From, #body{}},
+ State7#state.receivers),
+ {next_state, active, State7#state{prev_rid = RID,
+ receivers = Receivers}};
+ true ->
+ State6 = drop_holding_receiver(State5),
+ reply_next_state(State6#state{prev_rid = RID},
+ #body{els = RespEls}, RID, From)
+ end
+ end.
+
+handle_event({become_controller, C2SPid}, StateName, State) ->
+ State1 = route_els(State#state{c2s_pid = C2SPid}),
+ {next_state, StateName, State1};
+handle_event({change_shaper, Shaper}, StateName, State) ->
+ NewShaperState = shaper:new(Shaper),
+ {next_state, StateName, State#state{shaper_state = NewShaperState}};
+handle_event(_Event, StateName, State) ->
+ ?ERROR_MSG("unexpected event in '~s': ~p", [StateName, _Event]),
+ {next_state, StateName, State}.
+
+handle_sync_event({send_xml, {xmlstreamstart, _, _} = El}, _From,
+ StateName, State) when State#state.xmpp_ver >= "1.0" ->
+ %% Avoid sending empty <body/> element
+ OutBuf = buf_in([El], State#state.el_obuf),
+ {reply, ok, StateName, State#state{el_obuf = OutBuf}};
+handle_sync_event({send_xml, El}, _From, StateName, State) ->
+ OutBuf = buf_in([El], State#state.el_obuf),
+ State1 = State#state{el_obuf = OutBuf},
+ case gb_trees:lookup(State1#state.prev_rid, State1#state.receivers) of
+ {value, {From, Body}} ->
+ {State2, Els} = get_response_els(State1),
+ {reply, ok, StateName, reply(State2, Body#body{els = Els},
+ State2#state.prev_rid, From)};
+ none ->
+ State2 = case queue:out(State1#state.shaped_receivers) of
+ {{value, {TRef, From, Body}}, Q} ->
+ cancel_timer(TRef),
+ ?GEN_FSM:send_event(self(), {Body, From}),
+ State1#state{shaped_receivers = Q};
+ _ ->
+ State1
+ end,
+ {reply, ok, StateName, State2}
+ end;
+handle_sync_event(close, _From, _StateName, State) ->
+ {stop, normal, State};
+handle_sync_event(deactivate_socket, _From, StateName, StateData) ->
+ {reply, ok, StateName, StateData#state{c2s_pid = undefined}};
+handle_sync_event(_Event, _From, StateName, State) ->
+ ?ERROR_MSG("unexpected sync event in '~s': ~p", [StateName, _Event]),
+ {reply, {error, badarg}, StateName, State}.
+
+handle_info({timeout, TRef, wait_timeout}, StateName,
+ #state{wait_timer = TRef} = State) ->
+ {next_state, StateName, drop_holding_receiver(State)};
+handle_info({timeout, TRef, inactive}, _StateName,
+ #state{inactivity_timer = TRef} = State) ->
+ {stop, normal, State};
+handle_info({timeout, TRef, shaper_timeout}, StateName, State) ->
+ case queue:out(State#state.shaped_receivers) of
+ {{value, {TRef, From, Req}}, Q} ->
+ ?GEN_FSM:send_event(self(), {Req, From}),
+ {next_state, StateName, State#state{shaped_receivers = Q}};
+ {{value, _}, _} ->
+ ?ERROR_MSG("shaper_timeout mismatch:~n"
+ "** TRef: ~p~n"
+ "** State: ~p",
+ [TRef, State]),
+ {stop, normal, State};
+ _ ->
+ {next_state, StateName, State}
+ end;
+handle_info({migrate, Node}, StateName, State) ->
+ if Node /= node() ->
+ NewState = bounce_receivers(State, migrated),
+ {migrate, NewState,
+ {Node, ?MODULE, start, [StateName, NewState]}, 0};
+ true ->
+ {next_state, StateName, State}
+ end;
+handle_info(_Info, StateName, State) ->
+ ?ERROR_MSG("unexpected info:~n"
+ "** Msg: ~p~n"
+ "** StateName: ~p",
+ [_Info, StateName]),
+ {next_state, StateName, State}.
+
+terminate({migrated, ClonePid}, _StateName, State) ->
+ ?INFO_MSG("Migrating session \"~s\" (c2s_pid = ~p) to ~p on node ~p",
+ [State#state.sid, State#state.c2s_pid,
+ ClonePid, node(ClonePid)]),
+ mod_bosh:close_session(State#state.sid);
+terminate(_Reason, _StateName, State) ->
+ mod_bosh:close_session(State#state.sid),
+ case State#state.c2s_pid of
+ C2SPid when is_pid(C2SPid) ->
+ ?GEN_FSM:send_event(C2SPid, closed);
+ _ ->
+ ok
+ end,
+ bounce_receivers(State, closed),
+ bounce_els_from_obuf(State).
+
+code_change(_OldVsn, StateName, State, _Extra) ->
+ {ok, StateName, State}.
+
+print_state(State) ->
+ State.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+route_els(#state{el_ibuf = Buf} = State) ->
+ route_els(State#state{el_ibuf = buf_new()}, buf_to_list(Buf)).
+
+route_els(State, Els) ->
+ case State#state.c2s_pid of
+ C2SPid when is_pid(C2SPid) ->
+ lists:foreach(
+ fun(El) ->
+ ?GEN_FSM:send_event(C2SPid, El)
+ end, Els),
+ State;
+ _ ->
+ InBuf = buf_in(Els, State#state.el_ibuf),
+ State#state{el_ibuf = InBuf}
+ end.
+
+get_response_els(#state{el_obuf = OutBuf, max_concat = MaxConcat} = State) ->
+ {Els, NewOutBuf} = buf_out(OutBuf, MaxConcat),
+ {State#state{el_obuf = NewOutBuf}, Els}.
+
+reply(State, Body, RID, From) ->
+ State1 = restart_inactivity_timer(State),
+ Receivers = gb_trees:delete_any(RID, State1#state.receivers),
+ State2 = do_reply(State1, From, Body, RID),
+ case catch gb_trees:take_smallest(Receivers) of
+ {NextRID, {From1, Req}, Receivers1} when NextRID == RID + 1 ->
+ ?GEN_FSM:send_event(self(), {Req, From1}),
+ State2#state{receivers = Receivers1};
+ _ ->
+ State2#state{receivers = Receivers}
+ end.
+
+reply_next_state(State, Body, RID, From) ->
+ State1 = restart_inactivity_timer(State),
+ Receivers = gb_trees:delete_any(RID, State1#state.receivers),
+ State2 = do_reply(State1, From, Body, RID),
+ case catch gb_trees:take_smallest(Receivers) of
+ {NextRID, {From1, Req}, Receivers1} when NextRID == RID + 1 ->
+ active(Req, From1, State2#state{receivers = Receivers1});
+ _ ->
+ {next_state, active, State2#state{receivers = Receivers}}
+ end.
+
+reply_stop(State, Body, From, RID) ->
+ {stop, normal, do_reply(State, From, Body, RID)}.
+
+drop_holding_receiver(State) ->
+ RID = State#state.prev_rid,
+ case gb_trees:lookup(RID, State#state.receivers) of
+ {value, {From, Body}} ->
+ State1 = restart_inactivity_timer(State),
+ Receivers = gb_trees:delete_any(RID, State1#state.receivers),
+ State2 = State1#state{receivers = Receivers},
+ do_reply(State2, From, Body, RID);
+ none ->
+ State
+ end.
+
+do_reply(State, From, Body, RID) ->
+ ?DEBUG("send reply:~n"
+ "** RequestID: ~p~n"
+ "** Reply: ~p~n"
+ "** To: ~p~n"
+ "** State: ~p",
+ [RID, Body, From, State]),
+ ?GEN_FSM:reply(From, Body),
+ Responses = gb_trees:delete_any(RID, State#state.responses),
+ Responses1 = case gb_trees:size(Responses) of
+ N when N < State#state.max_requests; N == 0 ->
+ Responses;
+ _ ->
+ element(3, gb_trees:take_smallest(Responses))
+ end,
+ Responses2 = gb_trees:insert(RID, Body, Responses1),
+ State#state{responses = Responses2}.
+
+bounce_receivers(State, Reason) ->
+ Receivers = gb_trees:to_list(State#state.receivers),
+ ShapedReceivers = lists:map(
+ fun({_, From, #body{attrs = Attrs} = Body}) ->
+ RID = get_attr('rid', Attrs),
+ {RID, {From, Body}}
+ end, queue:to_list(State#state.shaped_receivers)),
+ lists:foldl(
+ fun({RID, {From, Body}}, AccState) ->
+ NewBody = if Reason == closed ->
+ #body{http_reason = "Session closed",
+ attrs = [{type, "terminate"},
+ {condition, "other-request"}]};
+ Reason == migrated ->
+ Body#body{http_reason = "Session migrated"}
+ end,
+ do_reply(AccState, From, NewBody, RID)
+ end, State, Receivers ++ ShapedReceivers).
+
+bounce_els_from_obuf(State) ->
+ lists:foreach(
+ fun({xmlstreamelement, El}) ->
+ case El of
+ {xmlelement, Name, Attrs, _}
+ when Name == "presence";
+ Name == "message";
+ Name == "iq" ->
+ FromS = xml:get_attr_s("from", Attrs),
+ ToS = xml:get_attr_s("to", Attrs),
+ case {jlib:string_to_jid(FromS),
+ jlib:string_to_jid(ToS)} of
+ {#jid{} = From, #jid{} = To} ->
+ ejabberd_router:route(From, To, El);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end;
+ (_) ->
+ ok
+ end, buf_to_list(State#state.el_obuf)).
+
+is_valid_key("", "") ->
+ true;
+is_valid_key([_|_] = PrevKey, [_|_] = Key) ->
+ sha:sha(Key) == PrevKey;
+is_valid_key(_, _) ->
+ false.
+
+is_overactivity(undefined) ->
+ false;
+is_overactivity(PrevPoll) ->
+ PollPeriod = timer:now_diff(now(), PrevPoll) div 1000000,
+ if PollPeriod < ?DEFAULT_POLLING ->
+ true;
+ true ->
+ false
+ end.
+
+make_xmlstreamstart(XMPPDomain, Version) ->
+ VersionEl = case Version of
+ "" -> [];
+ _ -> [{"version", Version}]
+ end,
+ {xmlstreamstart, "stream:stream",
+ [{"to", XMPPDomain},
+ {"xmlns", ?NS_CLIENT},
+ {"xmlns:xmpp", ?NS_BOSH},
+ {"xmlns:stream", ?NS_STREAM}|VersionEl]}.
+
+maybe_add_xmlstreamend(Els, "terminate") ->
+ Els ++ [{xmlstreamend, "stream:stream"}];
+maybe_add_xmlstreamend(Els, _) ->
+ Els.
+
+encode_body(#body{attrs = Attrs, els = Els}) ->
+ Attrs1 = lists:map(
+ fun({K, V}) when is_atom(K) ->
+ AmK = atom_to_list(K),
+ case V of
+ true -> {AmK, "true"};
+ false -> {AmK, "false"};
+ [_|_] -> {AmK, V};
+ I when is_integer(I), I >= 0 ->
+ {AmK, integer_to_list(I)}
+ end;
+ ({K, V}) ->
+ {K, V}
+ end, Attrs),
+ Attrs2 = [{"xmlns", ?NS_HTTP_BIND}|Attrs1],
+ {Attrs3, XMLs} =
+ lists:foldr(
+ fun({xmlstreamraw, XML}, {AttrsAcc, XMLBuf}) ->
+ {AttrsAcc, [XML|XMLBuf]};
+ ({xmlstreamelement, {xmlelement, "stream:error", _, _} = El},
+ {AttrsAcc, XMLBuf}) ->
+ {[{"type", "terminate"},
+ {"condition", "remote-stream-error"},
+ {"xmlns:stream", ?NS_STREAM}|AttrsAcc],
+ [xml:element_to_binary(El)|XMLBuf]};
+ ({xmlstreamelement, {xmlelement, "stream:features", _, _} = El}, {AttrsAcc, XMLBuf}) ->
+ {lists:keystore("xmlns:stream", 1, AttrsAcc, {"xmlns:stream", ?NS_STREAM}),
+ [xml:element_to_binary(El)|XMLBuf]};
+ ({xmlstreamelement, El}, {AttrsAcc, XMLBuf}) ->
+ {AttrsAcc, [xml:element_to_binary(El)|XMLBuf]};
+ ({xmlstreamend, _}, {AttrsAcc, XMLBuf}) ->
+ {[{"type", "terminate"},
+ {"condition", "remote-stream-error"}|AttrsAcc], XMLBuf};
+ ({xmlstreamstart, "stream:stream", SAttrs}, {AttrsAcc, XMLBuf}) ->
+ StreamID = xml:get_attr_s("id", SAttrs),
+ NewAttrs = case xml:get_attr_s("version", SAttrs) of
+ "" ->
+ [{"authid", StreamID}|AttrsAcc];
+ V ->
+ lists:keystore("xmlns:xmpp", 1, [{"xmpp:version", V},
+ {"authid", StreamID} | AttrsAcc],
+ {"xmlns:xmpp", ?NS_BOSH})
+ end,
+ {NewAttrs, XMLBuf};
+ ({xmlstreamerror, _}, {AttrsAcc, XMLBuf}) ->
+ {[{"type", "terminate"},
+ {"condition", "remote-stream-error"}|AttrsAcc],
+ XMLBuf};
+ (_, Acc) ->
+ Acc
+ end, {Attrs2, []}, Els),
+ case XMLs of
+ [] ->
+ ["<body", attrs_to_list(Attrs3), "/>"];
+ _ ->
+ ["<body", attrs_to_list(Attrs3), $>, XMLs, "</body>"]
+ end.
+
+decode_body(BodyXML, Size) ->
+ case xml_stream:parse_element(BodyXML) of
+ {xmlelement, "body", Attrs, Els} ->
+ case attrs_to_body_attrs(Attrs) of
+ {error, _} = Err ->
+ Err;
+ BodyAttrs ->
+ case get_attr(rid, BodyAttrs) of
+ "" ->
+ {error, "Missing \"rid\" attribute"};
+ _ ->
+ Els1 = lists:flatmap(
+ fun({xmlelement, _, _, _} = El) ->
+ [{xmlstreamelement, El}];
+ (_) ->
+ []
+ end, Els),
+ {ok, #body{attrs = BodyAttrs,
+ size = Size,
+ els = Els1}}
+ end
+ end;
+ {xmlelement, _, _, _} ->
+ {error, "Unexpected payload"};
+ _ ->
+ {error, "XML is not well-formed"}
+ end.
+
+attrs_to_body_attrs(Attrs) ->
+ lists:foldl(
+ fun(_, {error, Reason}) ->
+ {error, Reason};
+ ({Attr, Val}, Acc) ->
+ try
+ case Attr of
+ "ver" -> [{ver, Val}|Acc];
+ "xmpp:version" -> [{'xmpp:version', Val}|Acc];
+ "type" -> [{type, Val}|Acc];
+ "key" -> [{key, Val}|Acc];
+ "newkey" -> [{newkey, Val}|Acc];
+ "xmlns" -> Val = ?NS_HTTP_BIND, Acc;
+ "secure" -> [{secure, to_bool(Val)}|Acc];
+ "xmpp:restart" -> [{'xmpp:restart', to_bool(Val)}|Acc];
+ "to" -> [{to, [_|_] = jlib:nameprep(Val)}|Acc];
+ "wait" -> [{wait, to_int(Val, 0)}|Acc];
+ "ack" -> [{ack, to_int(Val, 0)}|Acc];
+ "sid" -> [{sid, Val}|Acc];
+ "hold" -> [{hold, to_int(Val, 0)}|Acc];
+ "rid" -> [{rid, to_int(Val, 0)}|Acc];
+ "pause" -> [{pause, to_int(Val, 0)}|Acc];
+ _ -> [{Attr, Val}|Acc]
+ end
+ catch _:_ ->
+ {error, "Invalid \"" ++ Attr ++ "\" attribute"}
+ end
+ end, [], Attrs).
+
+to_int(S, Min) ->
+ case list_to_integer(S) of
+ I when I >= Min ->
+ I;
+ _ ->
+ erlang:error(badarg)
+ end.
+
+to_bool("true") -> true;
+to_bool("1") -> true;
+to_bool("false") -> false;
+to_bool("0") -> false.
+
+attrs_to_list(Attrs) ->
+ [attr_to_list(A) || A <- Attrs].
+
+attr_to_list({Name, Value}) ->
+ [$\s, Name, $=, $', xml:crypt(Value), $'].
+
+bosh_response(Body) ->
+ {200, Body#body.http_reason, ?HEADER, encode_body(Body)}.
+
+http_error(Status, Reason) ->
+ {Status, Reason, ?HEADER, ""}.
+
+make_sid() ->
+ sha:sha(randoms:get_string()).
+
+-compile({no_auto_import,[min/2]}).
+min(undefined, B) -> B;
+min(A, B) -> erlang:min(A, B).
+
+%% Check that mod_bosh has been defined in config file.
+%% Print a warning in log file if this is not the case.
+check_bosh_module(XmppDomain) ->
+ case gen_mod:is_loaded(XmppDomain, mod_bosh) of
+ true -> ok;
+ false -> ?ERROR_MSG("You are trying to use BOSH (HTTP Bind) in host ~p,"
+ " but the module mod_bosh is not started in"
+ " that host. Configure your BOSH client to connect"
+ " to the correct host, or add your desired host to"
+ " the configuration, or check your 'modules'"
+ " section in your ejabberd configuration file.",
+ [XmppDomain])
+ end.
+
+get_attr(Attr, Attrs) ->
+ get_attr(Attr, Attrs, "").
+
+get_attr(Attr, Attrs, Default) ->
+ case lists:keysearch(Attr, 1, Attrs) of
+ {value, {_, Val}} ->
+ Val;
+ _ ->
+ Default
+ end.
+
+buf_new() ->
+ queue:new().
+
+buf_in(Xs, Buf) ->
+ lists:foldl(
+ fun(X, Acc) ->
+ queue:in(X, Acc)
+ end, Buf, Xs).
+
+buf_out(Buf, Num) when is_integer(Num), Num > 0 ->
+ buf_out(Buf, Num, []);
+buf_out(Buf, _) ->
+ {queue:to_list(Buf), buf_new()}.
+
+buf_out(Buf, 0, Els) ->
+ {lists:reverse(Els), Buf};
+buf_out(Buf, I, Els) ->
+ case queue:out(Buf) of
+ {{value, El}, NewBuf} ->
+ buf_out(NewBuf, I-1, [El|Els]);
+ {empty, _} ->
+ buf_out(Buf, 0, Els)
+ end.
+
+buf_to_list(Buf) ->
+ queue:to_list(Buf).
+
+cancel_timer(TRef) when is_reference(TRef) ->
+ ?GEN_FSM:cancel_timer(TRef);
+cancel_timer(_) ->
+ false.
+
+restart_timer(TRef, Timeout, Msg) ->
+ cancel_timer(TRef),
+ erlang:start_timer(timer:seconds(Timeout), self(), Msg).
+
+restart_inactivity_timer(#state{inactivity_timeout = Timeout} = State) ->
+ restart_inactivity_timer(State, Timeout).
+
+restart_inactivity_timer(#state{inactivity_timer = TRef} = State, Timeout) ->
+ NewTRef = restart_timer(TRef, Timeout, inactive),
+ State#state{inactivity_timer = NewTRef}.
+
+stop_inactivity_timer(#state{inactivity_timer = TRef} = State) ->
+ cancel_timer(TRef),
+ State#state{inactivity_timer = undefined}.
+
+restart_wait_timer(#state{wait_timer = TRef,
+ wait_timeout = Timeout} = State) ->
+ NewTRef = restart_timer(TRef, Timeout, wait_timeout),
+ State#state{wait_timer = NewTRef}.
+
+stop_wait_timer(#state{wait_timer = TRef} = State) ->
+ cancel_timer(TRef),
+ State#state{wait_timer = undefined}.
+
+start_shaper_timer(Timeout) ->
+ erlang:start_timer(Timeout, self(), shaper_timeout).
+
+make_random_jid(Host) ->
+ %% Copied from cyrsasl_anonymous.erl
+ User = lists:concat([randoms:get_string() | tuple_to_list(now())]),
+ jlib:make_jid(User, Host, randoms:get_string()).
+
+make_socket(Pid, IP) ->
+ {http_bind, Pid, IP}.
diff --git a/src/web/ejabberd_http.erl b/src/web/ejabberd_http.erl
index c20e267db..ffc78449f 100644
--- a/src/web/ejabberd_http.erl
+++ b/src/web/ejabberd_http.erl
@@ -66,7 +66,8 @@
request_headers = [],
end_of_request = false,
default_host,
- trail = <<>>
+ trail = <<>>,
+ websocket_handlers = []
}).
@@ -140,14 +141,18 @@ init({SockMod, Socket}, Opts) ->
false -> []
end,
?DEBUG("S: ~p~n", [RequestHandlers]),
-
+ WebSocketHandlers = case lists:keysearch(websocket_handlers, 1, Opts) of
+ {value, {websocket_handlers, WH}} -> WH;
+ false -> []
+ end,
+ ?DEBUG("WS: ~p~n", [WebSocketHandlers]),
DefaultHost = gen_mod:get_opt(default_host, Opts, undefined),
-
?INFO_MSG("started: ~p", [{SockMod1, Socket1}]),
State = #state{sockmod = SockMod1,
socket = Socket1,
default_host = DefaultHost,
- request_handlers = RequestHandlers},
+ request_handlers = RequestHandlers,
+ websocket_handlers = WebSocketHandlers},
receive_headers(State).
@@ -157,6 +162,9 @@ become_controller(_Pid) ->
socket_type() ->
raw.
+
+send_text(_State, none) ->
+ exit(normal);
send_text(State, Text) ->
case catch (State#state.sockmod):send(State#state.socket, Text) of
ok -> ok;
@@ -174,7 +182,7 @@ receive_headers(#state{trail=Trail} = State) ->
Data = SockMod:recv(Socket, 0, 300000),
case State#state.sockmod of
gen_tcp ->
- NewState = process_header(State, Data),
+ NewState = process_header(State, Data, true),
case NewState#state.end_of_request of
true ->
ok;
@@ -199,7 +207,7 @@ parse_headers(#state{request_method = Method, trail = Data} = State) ->
end,
case decode_packet(PktType, Data) of
{ok, Pkt, Rest} ->
- NewState = process_header(State#state{trail = Rest}, {ok, Pkt}),
+ NewState = process_header(State#state{trail = Rest}, {ok, Pkt}, false),
case NewState#state.end_of_request of
true ->
ok;
@@ -212,7 +220,7 @@ parse_headers(#state{request_method = Method, trail = Data} = State) ->
ok
end.
-process_header(State, Data) ->
+process_header(State, Data, Normalize) ->
SockMod = State#state.sockmod,
Socket = State#state.socket,
case Data of
@@ -259,6 +267,8 @@ 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_list(Name) andalso Normalize ->
+ 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)};
{ok, http_eoh} when State#state.request_host == undefined ->
@@ -303,6 +313,12 @@ process_header(State, Data) ->
add_header(Name, Value, State) ->
[{Name, Value} | State#state.request_headers].
+-define(GETOPT(Param, Opts),
+ case lists:keysearch(Param, 1, Opts) of
+ {value, {Param, V}} -> V;
+ false -> undefined
+ end).
+
get_host_really_served(undefined, Provided) ->
Provided;
get_host_really_served(Default, Provided) ->
@@ -335,8 +351,41 @@ 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, #ws{} = Ws)->
+ [{HandlerPathPrefix, HandlerModule, HandlerOpts} | HandlersLeft] = Handlers,
+ case (lists:prefix(HandlerPathPrefix, Ws#ws.path) or
+ (HandlerPathPrefix==Ws#ws.path)) of
+ true ->
+ LocalPath = lists:nthtail(length(HandlerPathPrefix), Ws#ws.path),
+ ejabberd_hooks:run(ws_debug, [{LocalPath, Ws}]),
+ Protocol = case lists:keysearch(protocol, 1, HandlerOpts) of
+ {value, {protocol, P}} -> P;
+ false -> undefined
+ end,
+ Origins = case lists:keysearch(origins, 1, HandlerOpts) of
+ {value, {origins, O}} -> O;
+ false -> []
+ end,
+ Auth = case lists:keysearch(auth, 1, HandlerOpts) of
+ {value, {auth, A}} -> A;
+ false -> undefined
+ end,
+ WS2 = Ws#ws{local_path = LocalPath,
+ protocol=Protocol,
+ acceptable_origins=Origins,
+ auth_module=Auth},
+ case ejabberd_websocket:is_acceptable(WS2) of
+ true ->
+ ejabberd_websocket:connect(WS2, HandlerModule);
+ false ->
+ process(HandlersLeft, Ws)
+ end;
+ false ->
+ process(HandlersLeft, Ws)
+ end;
process(Handlers, Request) ->
[{HandlerPathPrefix, HandlerModule} | HandlersLeft] = Handlers,
@@ -366,6 +415,7 @@ process_request(#state{request_method = Method,
request_tp = TP,
request_headers = RequestHeaders,
sockmod = SockMod,
+ websocket_handlers = WebSocketHandlers,
socket = Socket} = State)
when Method=:='GET' orelse Method=:='HEAD' orelse Method=:='DELETE' orelse Method=:='OPTIONS' ->
case (catch url_decode_q_split(Path)) of
@@ -388,31 +438,55 @@ process_request(#state{request_method = Method,
end,
XFF = proplists:get_value('X-Forwarded-For', RequestHeaders, []),
IP = analyze_ip_xff(IPHere, XFF, Host),
- Request = #request{method = Method,
- path = LPath,
- 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) == xmlelement ->
- make_xhtml_output(State, 200, [], El);
- {Status, Headers, El} when
- element(1, El) == xmlelement ->
- 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
+ case ejabberd_websocket:check(Path, RequestHeaders) of
+ {true, VSN} ->
+ {_, Origin} = case lists:keyfind("Sec-Websocket-Origin", 1, RequestHeaders) of
+ false -> lists:keyfind("Origin", 1, RequestHeaders);
+ Value -> Value
+ end,
+ Ws = #ws{socket = Socket,
+ sockmod = SockMod,
+ ws_autoexit = false,
+ ip = IP,
+ path = LPath,
+ q = LQuery,
+ vsn = VSN,
+ host = Host,
+ port = Port,
+ origin = Origin,
+ headers = RequestHeaders
+ },
+ process(WebSocketHandlers, Ws),
+ none;
+ false ->
+ Request = #request{method = Method,
+ path = LPath,
+ q = LQuery,
+ auth = Auth,
+ lang = Lang,
+ host = Host,
+ port = Port,
+ tp = TP,
+ headers = RequestHeaders,
+ ip = IP},
+ case process(RequestHandlers, Request) of
+ El when element(1, El) == xmlelement ->
+ make_xhtml_output(State, 200, [], El);
+ {Status, Headers, El} when
+ element(1, El) == xmlelement ->
+ 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);
+ {Status, Reason, Headers, Output} when is_list(Output) or is_binary(Output) ->
+ make_text_output(State, Status, Reason, Headers, Output)
+ end
+ end
end;
process_request(#state{request_method = Method,
@@ -476,7 +550,9 @@ process_request(#state{request_method = Method,
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)
+ make_text_output(State, Status, Headers, Output);
+ {Status, Reason, Headers, Output} when is_list(Output) or is_binary(Output) ->
+ make_text_output(State, Status, Reason, Headers, Output)
end
end;
@@ -585,10 +661,13 @@ make_xhtml_output(State, Status, Headers, XHTML) ->
[SL, H, "\r\n", Data2].
-make_text_output(State, Status, Headers, Text) when is_list(Text) ->
- make_text_output(State, Status, Headers, list_to_binary(Text));
+make_text_output(State, Status, Headers, Text) ->
+ make_text_output(State, Status, "", Headers, Text).
+
+make_text_output(State, Status, Reason, Headers, Text) when is_list(Text) ->
+ make_text_output(State, Status, Reason, Headers, list_to_binary(Text));
-make_text_output(State, Status, Headers, Data) when is_binary(Data) ->
+make_text_output(State, Status, Reason, Headers, Data) when is_binary(Data) ->
Headers1 = case lists:keysearch("Content-Type", 1, Headers) of
{value, _} ->
[{"Content-Length", integer_to_list(size(Data))} |
@@ -618,8 +697,12 @@ make_text_output(State, Status, Headers, Data) when is_binary(Data) ->
H = lists:map(fun({Attr, Val}) ->
[Attr, ": ", Val, "\r\n"]
end, HeadersOut),
+ NewReason = case Reason of
+ "" -> code_to_phrase(Status);
+ _ -> Reason
+ end,
SL = [Version, integer_to_list(Status), " ",
- code_to_phrase(Status), "\r\n"],
+ NewReason, "\r\n"],
Data2 = case State#state.request_method of
'HEAD' -> "";
@@ -1036,14 +1119,28 @@ tolower(C) when C >= $A andalso C =< $Z ->
tolower(C) ->
C.
+normalize_header_name(Name) ->
+ case parse_header_line(Name, "", true) of
+ {ok, RName, _} ->
+ lists:reverse(RName);
+ {eol, RName} ->
+ lists:reverse(RName)
+ end.
+
parse_header_line(Line) ->
- parse_header_line(Line, "", true).
+ case parse_header_line(Line, "", true) of
+ {ok, Name, Rest} ->
+ encode_header(lists:reverse(Name), Rest);
+ _ ->
+ bad_request
+ end.
+
-parse_header_line("", _, _) ->
- bad_request;
+parse_header_line("", Name, _) ->
+ {eol, Name};
parse_header_line(":" ++ Rest, Name, _) ->
- encode_header(lists:reverse(Name), Rest);
+ {ok, Name, Rest};
parse_header_line("-" ++ Rest, Name, _) ->
parse_header_line(Rest, "-" ++ Name, true);
parse_header_line([C | Rest], Name, true) ->
diff --git a/src/web/ejabberd_http.hrl b/src/web/ejabberd_http.hrl
index 7cd6da179..5f1625ed5 100644
--- a/src/web/ejabberd_http.hrl
+++ b/src/web/ejabberd_http.hrl
@@ -32,3 +32,23 @@
tp, % transfer protocol = http | https
headers
}).
+
+
+% Websocket Request
+-record(ws, {
+ socket, % the socket handling the request
+ sockmod, % gen_tcp | tls
+ ws_autoexit, % websocket process is automatically killed: true | false
+ ip, % peer IP | undefined
+ vsn, % {Maj,Min} | {'draft-hixie', Ver}
+ origin, % the originator
+ host, % the host
+ port,
+ path, % the websocket GET request path
+ headers, % [{Tag, Val}]
+ local_path,
+ q,
+ protocol,
+ acceptable_origins = [],
+ auth_module
+ }).
diff --git a/src/web/ejabberd_http_bind.erl b/src/web/ejabberd_http_bind.erl
index 02b8d27b0..cbbe68c33 100644
--- a/src/web/ejabberd_http_bind.erl
+++ b/src/web/ejabberd_http_bind.erl
@@ -13,7 +13,7 @@
-behaviour(gen_fsm).
%% External exports
--export([start_link/3,
+-export([start_link/4,
init/1,
handle_event/3,
handle_sync_event/4,
@@ -27,6 +27,7 @@
setopts/2,
controlling_process/2,
become_controller/2,
+ change_controller/2,
custom_receiver/1,
reset_stream/1,
change_shaper/2,
@@ -70,6 +71,7 @@
wait_timer,
ctime = 0,
timer,
+ jid,
pause=0,
unprocessed_req_list = [], % list of request that have been delayed for proper reordering: {Request, PID}
req_list = [], % list of requests (cache)
@@ -124,14 +126,24 @@
start(XMPPDomain, Sid, Key, IP) ->
?DEBUG("Starting session", []),
SupervisorProc = gen_mod:get_module_proc(XMPPDomain, ?PROCNAME_MHB),
- case catch supervisor:start_child(SupervisorProc, [Sid, Key, IP]) of
- {ok, Pid} -> {ok, Pid};
- _ -> check_bind_module(XMPPDomain),
- {error, "Cannot start HTTP bind session"}
+ case catch supervisor:start_child(SupervisorProc, [XMPPDomain, Sid, Key, IP]) of
+ {ok, Pid} ->
+ {ok, Pid};
+ {error, _} = Err ->
+ case check_bind_module(XMPPDomain) of
+ false ->
+ {error, "Cannot start HTTP bind session"};
+ true ->
+ ?ERROR_MSG("Cannot start HTTP bind session: ~p", [Err]),
+ Err
+ end;
+ Exit ->
+ ?ERROR_MSG("Cannot start HTTP bind session: ~p", [Exit]),
+ {error, Exit}
end.
-start_link(Sid, Key, IP) ->
- gen_fsm:start_link(?MODULE, [Sid, Key, IP], ?FSMOPTS).
+start_link(ServerHost, Sid, Key, IP) ->
+ gen_fsm:start_link(?MODULE, [ServerHost, Sid, Key, IP], ?FSMOPTS).
send({http_bind, FsmRef, _IP}, Packet) ->
gen_fsm:sync_send_all_state_event(FsmRef, {send, Packet}).
@@ -144,7 +156,18 @@ setopts({http_bind, FsmRef, _IP}, Opts) ->
true ->
gen_fsm:send_all_state_event(FsmRef, {activate, self()});
_ ->
- ok
+ case lists:member({active, false}, Opts) of
+ true ->
+ case catch gen_fsm:sync_send_all_state_event(
+ FsmRef, deactivate_socket) of
+ {'EXIT', _} ->
+ {error, einval};
+ Res ->
+ Res
+ end;
+ _ ->
+ ok
+ end
end.
controlling_process(_Socket, _Pid) ->
@@ -156,6 +179,9 @@ custom_receiver({http_bind, FsmRef, _IP}) ->
become_controller(FsmRef, C2SPid) ->
gen_fsm:send_all_state_event(FsmRef, {become_controller, C2SPid}).
+change_controller({http_bind, FsmRef, _IP}, C2SPid) ->
+ become_controller(FsmRef, C2SPid).
+
reset_stream({http_bind, _FsmRef, _IP}) ->
ok.
@@ -174,7 +200,6 @@ sockname(_Socket) ->
peername({http_bind, _FsmRef, IP}) ->
{ok, IP}.
-
%% Entry point for data coming from client through ejabberd HTTP server:
process_request(Data, IP) ->
Opts1 = ejabberd_c2s_config:get_c2s_limits(),
@@ -196,12 +221,12 @@ process_request(Data, IP) ->
"xmlns='" ++ ?NS_HTTP_BIND ++ "'/>"};
XmppDomain ->
%% create new session
- Sid = sha:sha(term_to_binary({now(), make_ref()})),
+ Sid = make_sid(),
case start(XmppDomain, Sid, "", IP) of
{error, _} ->
- {200, ?HEADER, "<body type='terminate' "
+ {500, ?HEADER, "<body type='terminate' "
"condition='internal-server-error' "
- "xmlns='" ++ ?NS_HTTP_BIND ++ "'>BOSH module not started</body>"};
+ "xmlns='" ++ ?NS_HTTP_BIND ++ "'>Internal Server Error</body>"};
{ok, Pid} ->
handle_session_start(
Pid, XmppDomain, Sid, Rid, Attrs,
@@ -227,10 +252,10 @@ process_request(Data, IP) ->
handle_http_put(Sid, Rid, Attrs, Payload2, PayloadSize,
StreamStart, IP);
{size_limit, Sid} ->
- case mnesia:dirty_read({http_bind, Sid}) of
- [] ->
+ case get_session(Sid) of
+ {error, _} ->
{404, ?HEADER, ""};
- [#http_bind{pid = FsmRef}] ->
+ {ok, #http_bind{pid = FsmRef}} ->
gen_fsm:sync_send_all_state_event(FsmRef, {stop, close}),
{200, ?HEADER, "<body type='terminate' "
"condition='undefined-condition' "
@@ -284,16 +309,19 @@ handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs,
end,
XmppVersion = xml:get_attr_s("xmpp:version", Attrs),
?DEBUG("Create session: ~p", [Sid]),
- mnesia:dirty_write(
- #http_bind{id = Sid,
- pid = Pid,
- to = {XmppDomain,
- XmppVersion},
- hold = Hold,
- wait = Wait,
- process_delay = Pdelay,
- version = Version
- }),
+ mnesia:async_dirty(
+ fun() ->
+ mnesia:write(
+ #http_bind{id = Sid,
+ pid = Pid,
+ to = {XmppDomain,
+ XmppVersion},
+ hold = Hold,
+ wait = Wait,
+ process_delay = Pdelay,
+ version = Version
+ })
+ end),
handle_http_put(Sid, Rid, Attrs, Payload, PayloadSize, true, IP).
%%%----------------------------------------------------------------------
@@ -307,7 +335,7 @@ handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs,
%% ignore |
%% {stop, StopReason}
%%----------------------------------------------------------------------
-init([Sid, Key, IP]) ->
+init([ServerHost, Sid, Key, IP]) ->
?DEBUG("started: ~p", [{Sid, Key, IP}]),
%% Read c2s options from the first ejabberd_c2s configuration in
@@ -322,15 +350,25 @@ init([Sid, Key, IP]) ->
Shaper = none,
ShaperState = shaper:new(Shaper),
Socket = {http_bind, self(), IP},
- ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket, Opts),
Timer = erlang:start_timer(?MAX_INACTIVITY, self(), []),
- {ok, loop, #state{id = Sid,
- key = Key,
- socket = Socket,
- shaper_state = ShaperState,
- max_inactivity = ?MAX_INACTIVITY,
- max_pause = ?MAX_PAUSE,
- timer = Timer}}.
+ State = #state{id = Sid,
+ key = Key,
+ socket = Socket,
+ shaper_state = ShaperState,
+ max_inactivity = ?MAX_INACTIVITY,
+ max_pause = ?MAX_PAUSE,
+ timer = Timer},
+ case gen_mod:get_module_opt(ServerHost, mod_http_bind,
+ prebind, false) of
+ true ->
+ JID = make_random_jid(ServerHost),
+ ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket,
+ [{jid, JID} | Opts]),
+ {ok, loop, State#state{jid = JID}};
+ false ->
+ ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket, Opts),
+ {ok, loop, State}
+ end.
%%----------------------------------------------------------------------
%% Func: handle_event/3
@@ -339,6 +377,7 @@ init([Sid, Key, IP]) ->
%% {stop, Reason, NewStateData}
%%----------------------------------------------------------------------
handle_event({become_controller, C2SPid}, StateName, StateData) ->
+ erlang:monitor(process, C2SPid),
case StateData#state.input of
cancel ->
{next_state, StateName, StateData#state{
@@ -408,6 +447,14 @@ handle_sync_event({stop,close}, _From, _StateName, StateData) ->
handle_sync_event({stop,stream_closed}, _From, _StateName, StateData) ->
Reply = ok,
{stop, normal, Reply, StateData};
+handle_sync_event(deactivate_socket, _From, StateName, StateData) ->
+ %% Input = case StateData#state.input of
+ %% cancel ->
+ %% queue:new();
+ %% Q ->
+ %% Q
+ %% end,
+ {reply, ok, StateName, StateData#state{waiting_input = false}};
handle_sync_event({stop,Reason}, _From, _StateName, StateData) ->
?DEBUG("Closing bind session ~p - Reason: ~p", [StateData#state.id, Reason]),
Reply = ok,
@@ -439,6 +486,11 @@ handle_sync_event(#http_put{payload_size = PayloadSize} = Request,
shaper_timer = NewShaperTimer});
%% HTTP GET: send packets to the client
+handle_sync_event({http_get, _Rid, _Wait, _Hold}, _From,
+ StateName, #state{jid = JID} = StateData)
+ when JID /= undefined ->
+ %% This is a pre-bind state
+ {reply, {ok, {prebind, JID}}, StateName, StateData#state{jid = undefined}};
handle_sync_event({http_get, Rid, Wait, Hold}, From, StateName, StateData) ->
%% setup timer
TNow = tnow(),
@@ -447,6 +499,7 @@ handle_sync_event({http_get, Rid, Wait, Hold}, From, StateName, StateData) ->
((StateData#state.output == []) or (StateData#state.rid < Rid)) and
((TNow - StateData#state.ctime) < (Wait*1000*1000)) and
(StateData#state.rid =< Rid) and
+ (StateData#state.input /= cancel) and
(StateData#state.pause == 0) ->
send_receiver_reply(StateData#state.http_receiver, {ok, empty}),
cancel_timer(StateData#state.wait_timer),
@@ -543,6 +596,9 @@ handle_info({timeout, ShaperTimer, _}, StateName,
#state{shaper_timer = ShaperTimer} = StateData) ->
{next_state, StateName, StateData#state{shaper_timer = undefined}};
+handle_info({'DOWN', _MRef, process, C2SPid, _}, _StateName,
+ #state{waiting_input = C2SPid} = StateData) ->
+ {stop, normal, StateData};
handle_info(_, StateName, StateData) ->
{next_state, StateName, StateData}.
@@ -719,15 +775,16 @@ process_http_put(#http_put{rid = Rid, attrs = Attrs, payload = Payload,
ip = IP
});
C2SPid ->
+ JID = StateData#state.jid,
case StreamTo of
- {To, ""} ->
+ {To, ""} when JID == undefined ->
gen_fsm:send_event(
C2SPid,
{xmlstreamstart, "stream:stream",
[{"to", To},
{"xmlns", ?NS_CLIENT},
{"xmlns:stream", ?NS_STREAM}]});
- {To, Version} ->
+ {To, Version} when JID == undefined ->
gen_fsm:send_event(
C2SPid,
{xmlstreamstart, "stream:stream",
@@ -813,10 +870,10 @@ handle_http_put(Sid, Rid, Attrs, Payload, PayloadSize, StreamStart, IP) ->
http_put(Sid, Rid, Attrs, Payload, PayloadSize, StreamStart, IP) ->
?DEBUG("Looking for session: ~p", [Sid]),
- case mnesia:dirty_read({http_bind, Sid}) of
- [] ->
+ case get_session(Sid) of
+ {error, _} ->
{error, not_exists};
- [#http_bind{pid = FsmRef, hold=Hold, to={To, StreamVersion}}=Sess] ->
+ {ok, #http_bind{pid = FsmRef, hold=Hold, to={To, StreamVersion}}=Sess}->
NewStream =
case StreamStart of
true ->
@@ -824,10 +881,16 @@ http_put(Sid, Rid, Attrs, Payload, PayloadSize, StreamStart, IP) ->
_ ->
""
end,
- {gen_fsm:sync_send_all_state_event(
- FsmRef, #http_put{rid = Rid, attrs = Attrs, payload = Payload,
- payload_size = PayloadSize, hold = Hold,
- stream = NewStream, ip = IP}, 30000), Sess}
+ case catch {gen_fsm:sync_send_all_state_event(
+ FsmRef,
+ #http_put{rid = Rid, attrs = Attrs, payload = Payload,
+ payload_size = PayloadSize, hold = Hold,
+ stream = NewStream, ip = IP}, 30000), Sess} of
+ {'EXIT', _} ->
+ {error, not_exists};
+ Res ->
+ Res
+ end
end.
handle_http_put_error(Reason, #http_bind{pid=FsmRef, version=Version})
@@ -922,6 +985,16 @@ prepare_response(Sess, Rid, OutputEls, StreamStart) ->
{200, ?HEADER, "<body xmlns='"++?NS_HTTP_BIND++"'/>"};
{ok, terminate} ->
{200, ?HEADER, "<body type='terminate' xmlns='"++?NS_HTTP_BIND++"'/>"};
+ {ok, {prebind, JID}} ->
+ {200, ?HEADER,
+ xml:element_to_string(
+ {xmlelement, "body",
+ [{"xmlns", ?NS_HTTP_BIND}, {"sid", Sess#http_bind.id},
+ {"rid", integer_to_list(Rid + 1)}],
+ [{xmlelement, "iq", [{"id", "pre_bind"}, {"type", "result"}],
+ [{xmlelement, "bind", [{"xmlns", ?NS_BIND}],
+ [{xmlelement, "jid", [],
+ [{xmlcdata, jlib:jid_to_string(JID)}]}]}]}]})};
{ok, ROutPacket} ->
OutPacket = lists:reverse(ROutPacket),
?DEBUG("OutPacket: ~p", [OutputEls++OutPacket]),
@@ -1252,3 +1325,36 @@ check_bind_module(XmppDomain) ->
" section in your ejabberd configuration file.",
[XmppDomain])
end.
+
+make_sid() ->
+ sha:sha(term_to_binary({now(), make_ref()}))
+ ++ "-" ++ ejabberd_cluster:node_id().
+
+get_session(SID) ->
+ case string:tokens(SID, "-") of
+ [_, NodeID] ->
+ case ejabberd_cluster:get_node_by_id(NodeID) of
+ Node when Node == node() ->
+ case mnesia:dirty_read({http_bind, SID}) of
+ [] ->
+ {error, enoent};
+ [Session] ->
+ {ok, Session}
+ end;
+ Node ->
+ case catch rpc:call(Node, mnesia, dirty_read,
+ [{http_bind, SID}], 5000) of
+ [Session] ->
+ {ok, Session};
+ _ ->
+ {error, enoent}
+ end
+ end;
+ _ ->
+ {error, enoent}
+ end.
+
+make_random_jid(Host) ->
+ %% Copied from cyrsasl_anonymous.erl
+ User = lists:concat([randoms:get_string() | tuple_to_list(now())]),
+ jlib:make_jid(User, Host, randoms:get_string()).
diff --git a/src/web/ejabberd_http_bindjson.erl b/src/web/ejabberd_http_bindjson.erl
new file mode 100644
index 000000000..61f8779e8
--- /dev/null
+++ b/src/web/ejabberd_http_bindjson.erl
@@ -0,0 +1,1294 @@
+%%%----------------------------------------------------------------------
+%%% File : ejabberd_http_bindjson.erl
+%%% Original Bind Author : Stefan Strigler <steve@zeank.in-berlin.de>
+%%% Purpose : Implements XMPP over BOSH (XEP-0205) with a JSON Transport
+%%% Created : 23 Sep 2010 by Eric Cestari <ecestari@process-one.net>
+%%% Modified: may 2009 by Mickael Remond, Alexey Schepin
+%%% Id : $Id: ejabberd_http_bind.erl 953 2009-05-07 10:40:40Z alexey $
+%%%----------------------------------------------------------------------
+
+-module (ejabberd_http_bindjson).
+
+-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,
+ send_xml/2,
+ sockname/1,
+ peername/1,
+ setopts/2,
+ controlling_process/2,
+ become_controller/2,
+ change_controller/2,
+ custom_receiver/1,
+ reset_stream/1,
+ change_shaper/2,
+ monitor/1,
+ close/1,
+ start/4,
+ handle_session_start/8,
+ handle_http_put/7,
+ http_put/7,
+ http_get/2,
+ prepare_response/4,
+ process_request/2]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("ejabberd_http.hrl").
+-include("http_bind.hrl").
+
+-record(http_bind, {id, pid, to, hold, wait, process_delay, version}).
+
+-define(NULL_PEER, {{0, 0, 0, 0}, 0}).
+
+%% http binding request
+-record(hbr, {rid,
+ key,
+ out}).
+
+-record(state, {id,
+ rid = none,
+ key,
+ socket,
+ output = "",
+ input = queue:new(),
+ waiting_input = false,
+ shaper_state,
+ shaper_timer,
+ last_receiver,
+ last_poll,
+ http_receiver,
+ wait_timer,
+ ctime = 0,
+ timer,
+ pause=0,
+ unprocessed_req_list = [], % list of request that have been delayed for proper reordering: {Request, PID}
+ req_list = [], % list of requests (cache)
+ max_inactivity,
+ max_pause,
+ ip = ?NULL_PEER
+ }).
+
+%% Internal request format:
+-record(http_put, {rid,
+ attrs,
+ payload,
+ payload_size,
+ hold,
+ stream,
+ ip}).
+
+%%-define(DBGFSM, true).
+-ifdef(DBGFSM).
+-define(FSMOPTS, [{debug, [trace]}]).
+-else.
+-define(FSMOPTS, []).
+-endif.
+
+-define(BOSH_VERSION, "1.8").
+-define(NS_CLIENT, "jabber:client").
+-define(NS_BOSH, "urn:xmpp:xbosh").
+-define(NS_HTTP_BIND, "http://jabber.org/protocol/httpbind").
+
+-define(MAX_REQUESTS, 2). % number of simultaneous requests
+-define(MIN_POLLING, 2000000). % don't poll faster than that or we will
+ % shoot you (time in microsec)
+-define(MAX_WAIT, 3600). % max num of secs to keep a request on hold
+-define(MAX_INACTIVITY, 30000). % msecs to wait before terminating
+ % idle sessions
+-define(MAX_PAUSE, 120). % may num of sec a client is allowed to pause
+ % the session
+
+%% Wait 100ms before continue processing, to allow the client provide more related stanzas.
+-define(PROCESS_DELAY_DEFAULT, 100).
+-define(PROCESS_DELAY_MIN, 0).
+-define(PROCESS_DELAY_MAX, 1000).
+
+
+%%%----------------------------------------------------------------------
+%%% API
+%%%----------------------------------------------------------------------
+%% TODO: If compile with no supervisor option, start the session without
+%% supervisor
+start(XMPPDomain, Sid, Key, IP) ->
+ ?DEBUG("Starting session", []),
+ case catch supervisor:start_child(ejabberd_http_bind_sup, [Sid, Key, IP]) of
+ {ok, Pid} ->
+ {ok, Pid};
+ {error, _} = Err ->
+ case check_bind_module(XMPPDomain) of
+ false ->
+ {error, "Cannot start HTTP bind session"};
+ true ->
+ ?ERROR_MSG("Cannot start HTTP bind session: ~p", [Err]),
+ Err
+ end;
+ Exit ->
+ ?ERROR_MSG("Cannot start HTTP bind session: ~p", [Exit]),
+ {error, Exit}
+ end.
+
+start_link(Sid, Key, IP) ->
+ gen_fsm:start_link(?MODULE, [Sid, Key, IP], ?FSMOPTS).
+
+send({http_bind, FsmRef, _IP}, Packet) ->
+ gen_fsm:sync_send_all_state_event(FsmRef, {send, Packet}).
+
+send_xml({http_bind, FsmRef, _IP}, Packet) ->
+ gen_fsm:sync_send_all_state_event(FsmRef, {send_xml, Packet}).
+
+setopts({http_bind, FsmRef, _IP}, Opts) ->
+ case lists:member({active, once}, Opts) of
+ true ->
+ gen_fsm:send_all_state_event(FsmRef, {activate, self()});
+ _ ->
+ case lists:member({active, false}, Opts) of
+ true ->
+ gen_fsm:sync_send_all_state_event(
+ FsmRef, deactivate_socket);
+ _ ->
+ ok
+ end
+ end.
+
+controlling_process(_Socket, _Pid) ->
+ ok.
+
+custom_receiver({http_bind, FsmRef, _IP}) ->
+ {receiver, ?MODULE, FsmRef}.
+
+become_controller(FsmRef, C2SPid) ->
+ gen_fsm:send_all_state_event(FsmRef, {become_controller, C2SPid}).
+
+change_controller({http_bind, FsmRef, _IP}, C2SPid) ->
+ become_controller(FsmRef, C2SPid).
+
+reset_stream({http_bind, _FsmRef, _IP}) ->
+ ok.
+
+change_shaper({http_bind, FsmRef, _IP}, Shaper) ->
+ gen_fsm:send_all_state_event(FsmRef, {change_shaper, Shaper}).
+
+monitor({http_bind, FsmRef, _IP}) ->
+ erlang:monitor(process, FsmRef).
+
+close({http_bind, FsmRef, _IP}) ->
+ catch gen_fsm:sync_send_all_state_event(FsmRef, {stop, close}).
+
+sockname(_Socket) ->
+ {ok, ?NULL_PEER}.
+
+peername({http_bind, _FsmRef, IP}) ->
+ {ok, IP}.
+
+%% Entry point for data coming from client through ejabberd HTTP server:
+process_request(Data, IP) ->
+ Opts1 = ejabberd_c2s_config:get_c2s_limits(),
+ Opts = [{xml_socket, true} | Opts1],
+ MaxStanzaSize =
+ case lists:keysearch(max_stanza_size, 1, Opts) of
+ {value, {_, Size}} -> Size;
+ _ -> infinity
+ end,
+ PayloadSize = iolist_size(Data),
+ case catch parse_request(Data, PayloadSize, MaxStanzaSize) of
+ %% No existing session:
+ {ok, {"", Rid, Attrs, Payload}} ->
+ case xml:get_attr_s("to",Attrs) of
+ "" ->
+ ?DEBUG("Session not created (Improper addressing)", []),
+ {200, ?HEADER, "{\"body\":{\"type\":\"terminate\" "
+ "\"condition\":\"improper-addressing\", "
+ "\"xmlns\":\"" ++ ?NS_HTTP_BIND ++ "\"}}"};
+ XmppDomain ->
+ %% create new session
+ Sid = make_sid(),
+ case start(XmppDomain, Sid, "", IP) of
+ {error, _} ->
+ {500, ?HEADER,"{\"body\":{\"type\":\"terminate\" "
+ "\"condition\":\"internal-server-error\", "
+ "\"xmlns\":\"" ++ ?NS_HTTP_BIND ++ "\",\"$\":\"Internal Server Error\"}}"};
+ {ok, Pid} ->
+ handle_session_start(
+ Pid, XmppDomain, Sid, Rid, Attrs,
+ Payload, PayloadSize, IP)
+ end
+ end;
+ %% Existing session
+ {ok, {Sid, Rid, Attrs, Payload1}} ->
+ StreamStart =
+ case xml:get_attr_s("xmpp:restart",Attrs) of
+ "true" ->
+ true;
+ _ ->
+ false
+ end,
+ Payload2 = case xml:get_attr_s("type",Attrs) of
+ "terminate" ->
+ %% close stream
+ Payload1 ++ [{xmlstreamend, "stream:stream"}];
+ _ ->
+ Payload1
+ end,
+ handle_http_put(Sid, Rid, Attrs, Payload2, PayloadSize,
+ StreamStart, IP);
+ {size_limit, Sid} ->
+ case get_session(Sid) of
+ {error, _} ->
+ {404, ?HEADER, ""};
+ {ok, #http_bind{pid = FsmRef}} ->
+ gen_fsm:sync_send_all_state_event(FsmRef, {stop, close}),
+ {200, ?HEADER, "{\"body\": {\"type\"=\"terminate\" "
+ "\"condition\":\"undefined-condition\" "
+ "\"xmlns\":\"" ++ ?NS_HTTP_BIND ++ "\", \"$\":\"Request Too Large\"}}"}
+ end;
+ _ ->
+ ?DEBUG("Received bad request: ~p", [Data]),
+ {400, ?HEADER, ""}
+ end.
+
+handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs,
+ Payload, PayloadSize, IP) ->
+ ?DEBUG("got pid: ~p", [Pid]),
+ Wait = case string:to_integer(xml:get_attr_s("wait",Attrs)) of
+ {error, _} ->
+ ?MAX_WAIT;
+ {CWait, _} ->
+ if
+ (CWait > ?MAX_WAIT) ->
+ ?MAX_WAIT;
+ true ->
+ CWait
+ end
+ end,
+ Hold = case string:to_integer(xml:get_attr_s("hold",Attrs)) of
+ {error, _} ->
+ (?MAX_REQUESTS - 1);
+ {CHold, _} ->
+ if
+ (CHold > (?MAX_REQUESTS - 1)) ->
+ (?MAX_REQUESTS - 1);
+ true ->
+ CHold
+ end
+ end,
+ Pdelay = case string:to_integer(xml:get_attr_s("process-delay",Attrs)) of
+ {error, _} ->
+ ?PROCESS_DELAY_DEFAULT;
+ {CPdelay, _} when
+ (?PROCESS_DELAY_MIN =< CPdelay) and
+ (CPdelay =< ?PROCESS_DELAY_MAX) ->
+ CPdelay;
+ {CPdelay, _} ->
+ erlang:max(
+ erlang:min(CPdelay,?PROCESS_DELAY_MAX),
+ ?PROCESS_DELAY_MIN)
+ end,
+ Version =
+ case catch list_to_float(
+ xml:get_attr_s("ver", Attrs)) of
+ {'EXIT', _} -> 0.0;
+ V -> V
+ end,
+ XmppVersion = xml:get_attr_s("xmpp:version", Attrs),
+ ?DEBUG("Create session: ~p", [Sid]),
+ mnesia:async_dirty(
+ fun() ->
+ mnesia:write(
+ #http_bind{id = Sid,
+ pid = Pid,
+ to = {XmppDomain,
+ XmppVersion},
+ hold = Hold,
+ wait = Wait,
+ process_delay = Pdelay,
+ version = Version
+ })
+ end),
+ handle_http_put(Sid, Rid, Attrs, Payload, PayloadSize, true, IP).
+
+%%%----------------------------------------------------------------------
+%%% Callback functions from gen_fsm
+%%%----------------------------------------------------------------------
+
+%%----------------------------------------------------------------------
+%% Func: init/1
+%% Returns: {ok, StateName, StateData} |
+%% {ok, StateName, StateData, Timeout} |
+%% ignore |
+%% {stop, StopReason}
+%%----------------------------------------------------------------------
+init([Sid, Key, IP]) ->
+ ?DEBUG("started: ~p", [{Sid, Key, IP}]),
+
+ %% Read c2s options from the first ejabberd_c2s configuration in
+ %% the config file listen section
+ %% TODO: We should have different access and shaper values for
+ %% each connector. The default behaviour should be however to use
+ %% the default c2s restrictions if not defined for the current
+ %% connector.
+ Opts1 = ejabberd_c2s_config:get_c2s_limits(),
+ Opts = [{xml_socket, true} | Opts1],
+
+ Shaper = none,
+ ShaperState = shaper:new(Shaper),
+ Socket = {http_bind, self(), IP},
+ ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket, Opts),
+ Timer = erlang:start_timer(?MAX_INACTIVITY, self(), []),
+ {ok, loop, #state{id = Sid,
+ key = Key,
+ socket = Socket,
+ shaper_state = ShaperState,
+ max_inactivity = ?MAX_INACTIVITY,
+ max_pause = ?MAX_PAUSE,
+ timer = Timer}}.
+
+%%----------------------------------------------------------------------
+%% Func: handle_event/3
+%% Returns: {next_state, NextStateName, NextStateData} |
+%% {next_state, NextStateName, NextStateData, Timeout} |
+%% {stop, Reason, NewStateData}
+%%----------------------------------------------------------------------
+handle_event({become_controller, C2SPid}, StateName, StateData) ->
+ erlang:monitor(process, C2SPid),
+ case StateData#state.input of
+ cancel ->
+ {next_state, StateName, StateData#state{
+ waiting_input = C2SPid}};
+ Input ->
+ lists:foreach(
+ fun(Event) ->
+ C2SPid ! Event
+ end, queue:to_list(Input)),
+ {next_state, StateName, StateData#state{
+ input = queue:new(),
+ waiting_input = C2SPid}}
+ end;
+
+handle_event({change_shaper, Shaper}, StateName, StateData) ->
+ NewShaperState = shaper:new(Shaper),
+ {next_state, StateName, StateData#state{shaper_state = NewShaperState}};
+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_xml, Packet}, _From, StateName,
+ #state{http_receiver = undefined} = StateData) ->
+ Output = [Packet | StateData#state.output],
+ Reply = ok,
+ {reply, Reply, StateName, StateData#state{output = Output}};
+handle_sync_event({send_xml, Packet}, _From, StateName, StateData) ->
+ Output = [Packet | StateData#state.output],
+ cancel_timer(StateData#state.timer),
+ Timer = set_inactivity_timer(StateData#state.pause,
+ StateData#state.max_inactivity),
+ HTTPReply = {ok, Output},
+ gen_fsm:reply(StateData#state.http_receiver, HTTPReply),
+ cancel_timer(StateData#state.wait_timer),
+ Rid = StateData#state.rid,
+ ReqList = [#hbr{rid = Rid,
+ key = StateData#state.key,
+ out = Output
+ } |
+ [El || El <- StateData#state.req_list,
+ El#hbr.rid /= Rid ]
+ ],
+ Reply = ok,
+ {reply, Reply, StateName,
+ StateData#state{output = [],
+ http_receiver = undefined,
+ req_list = ReqList,
+ wait_timer = undefined,
+ timer = Timer}};
+
+handle_sync_event({stop,close}, _From, _StateName, StateData) ->
+ Reply = ok,
+ {stop, normal, Reply, StateData};
+handle_sync_event({stop,stream_closed}, _From, _StateName, StateData) ->
+ Reply = ok,
+ {stop, normal, Reply, StateData};
+handle_sync_event(deactivate_socket, _From, StateName, StateData) ->
+ %% Input = case StateData#state.input of
+ %% cancel ->
+ %% queue:new();
+ %% Q ->
+ %% Q
+ %% end,
+ {reply, ok, StateName, StateData#state{waiting_input = false}};
+handle_sync_event({stop,Reason}, _From, _StateName, StateData) ->
+ ?DEBUG("Closing bind session ~p - Reason: ~p", [StateData#state.id, Reason]),
+ Reply = ok,
+ {stop, normal, Reply, StateData};
+
+%% HTTP PUT: Receive packets from the client
+handle_sync_event(#http_put{rid = Rid},
+ _From, StateName, StateData)
+ when StateData#state.shaper_timer /= undefined ->
+ Pause =
+ case erlang:read_timer(StateData#state.shaper_timer) of
+ false ->
+ 0;
+ P -> P
+ end,
+ Reply = {wait, Pause},
+ ?DEBUG("Shaper timer for RID ~p: ~p", [Rid, Reply]),
+ {reply, Reply, StateName, StateData};
+
+handle_sync_event(#http_put{payload_size = PayloadSize} = Request,
+ _From, StateName, StateData) ->
+ ?DEBUG("New request: ~p",[Request]),
+ %% Updating trafic shaper
+ {NewShaperState, NewShaperTimer} =
+ update_shaper(StateData#state.shaper_state, PayloadSize),
+
+ handle_http_put_event(Request, StateName,
+ StateData#state{shaper_state = NewShaperState,
+ shaper_timer = NewShaperTimer});
+
+%% HTTP GET: send packets to the client
+handle_sync_event({http_get, Rid, Wait, Hold}, From, StateName, StateData) ->
+ %% setup timer
+ send_receiver_reply(StateData#state.http_receiver, {ok, empty}),
+ cancel_timer(StateData#state.wait_timer),
+ TNow = tnow(),
+ if
+ (Hold > 0) and
+ (StateData#state.output == []) and
+ ((TNow - StateData#state.ctime) < (Wait*1000*1000)) and
+ (StateData#state.rid == Rid) and
+ (StateData#state.input /= cancel) and
+ (StateData#state.pause == 0) ->
+ WaitTimer = erlang:start_timer(Wait * 1000, self(), []),
+ %% MR: Not sure we should cancel the state timer here.
+ cancel_timer(StateData#state.timer),
+ {next_state, StateName, StateData#state{
+ http_receiver = From,
+ wait_timer = WaitTimer,
+ timer = undefined}};
+ (StateData#state.input == cancel) ->
+ cancel_timer(StateData#state.timer),
+ Timer = set_inactivity_timer(StateData#state.pause,
+ StateData#state.max_inactivity),
+ Reply = {ok, cancel},
+ {reply, Reply, StateName, StateData#state{
+ input = queue:new(),
+ http_receiver = undefined,
+ wait_timer = undefined,
+ timer = Timer}};
+ true ->
+ cancel_timer(StateData#state.timer),
+ Timer = set_inactivity_timer(StateData#state.pause,
+ StateData#state.max_inactivity),
+ Reply = {ok, StateData#state.output},
+ %% save request
+ ReqList = [#hbr{rid = Rid,
+ key = StateData#state.key,
+ out = StateData#state.output
+ } |
+ [El || El <- StateData#state.req_list,
+ El#hbr.rid /= Rid ]
+ ],
+ {reply, Reply, StateName, StateData#state{
+ output = [],
+ http_receiver = undefined,
+ wait_timer = undefined,
+ timer = Timer,
+ req_list = ReqList}}
+ end;
+
+handle_sync_event(peername, _From, StateName, StateData) ->
+ Reply = {ok, StateData#state.ip},
+ {reply, Reply, StateName, StateData};
+
+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}
+%%----------------------------------------------------------------------
+%% We reached the max_inactivity timeout:
+handle_info({timeout, Timer, _}, _StateName,
+ #state{id=SID, timer = Timer} = StateData) ->
+ ?INFO_MSG("Session timeout. Closing the HTTP bind session: ~p", [SID]),
+ {stop, normal, StateData};
+
+handle_info({timeout, WaitTimer, _}, StateName,
+ #state{wait_timer = WaitTimer} = StateData) ->
+ if
+ StateData#state.http_receiver /= undefined ->
+ cancel_timer(StateData#state.timer),
+ Timer = set_inactivity_timer(StateData#state.pause,
+ StateData#state.max_inactivity),
+ gen_fsm:reply(StateData#state.http_receiver, {ok, empty}),
+ Rid = StateData#state.rid,
+ ReqList = [#hbr{rid = Rid,
+ key = StateData#state.key,
+ out = []
+ } |
+ [El || El <- StateData#state.req_list,
+ El#hbr.rid /= Rid ]
+ ],
+ {next_state, StateName,
+ StateData#state{http_receiver = undefined,
+ req_list = ReqList,
+ wait_timer = undefined,
+ timer = Timer}};
+ true ->
+ {next_state, StateName, StateData}
+ end;
+
+handle_info({timeout, ShaperTimer, _}, StateName,
+ #state{shaper_timer = ShaperTimer} = StateData) ->
+ {next_state, StateName, StateData#state{shaper_timer = undefined}};
+
+handle_info({'DOWN', _MRef, process, C2SPid, _}, _StateName,
+ #state{waiting_input = C2SPid} = 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) ->
+ ?DEBUG("terminate: Deleting session ~s", [StateData#state.id]),
+ mnesia:dirty_delete({http_bind, StateData#state.id}),
+ send_receiver_reply(StateData#state.http_receiver, {ok, terminate}),
+ case StateData#state.waiting_input of
+ false ->
+ ok;
+ C2SPid ->
+ gen_fsm:send_event(C2SPid, closed)
+ end,
+ ok.
+
+%%%----------------------------------------------------------------------
+%%% Internal functions
+%%%----------------------------------------------------------------------
+
+%% PUT / Get processing:
+handle_http_put_event(#http_put{rid = Rid, attrs = Attrs,
+ hold = Hold} = Request,
+ StateName, StateData) ->
+ ?DEBUG("New request: ~p",[Request]),
+ %% Check if Rid valid
+ RidAllow = rid_allow(StateData#state.rid, Rid, Attrs, Hold,
+ StateData#state.max_pause),
+
+ %% Check if Rid is in sequence or out of sequence:
+ case RidAllow of
+ buffer ->
+ ?DEBUG("Buffered request: ~p", [Request]),
+ %% Request is out of sequence:
+ PendingRequests = StateData#state.unprocessed_req_list,
+ %% In case an existing RID was already buffered:
+ Requests = lists:keydelete(Rid, 2, PendingRequests),
+ ReqList = [#hbr{rid = Rid,
+ key = StateData#state.key,
+ out = []
+ } |
+ [El || El <- StateData#state.req_list,
+ El#hbr.rid > (Rid - 1 - Hold)]
+ ],
+ ?DEBUG("reqlist: ~p", [ReqList]),
+ UnprocessedReqList = [Request | Requests],
+ cancel_timer(StateData#state.timer),
+ Timer = set_inactivity_timer(0, StateData#state.max_inactivity),
+ {reply, buffered, StateName,
+ StateData#state{unprocessed_req_list = UnprocessedReqList,
+ req_list = ReqList,
+ timer = Timer}};
+ _ ->
+ %% Request is in sequence:
+ process_http_put(Request, StateName, StateData, RidAllow)
+ end.
+
+process_http_put(#http_put{rid = Rid, attrs = Attrs, payload = Payload,
+ hold = Hold, stream = StreamTo,
+ ip = IP} = Request,
+ StateName, StateData, RidAllow) ->
+ ?DEBUG("Actually processing request: ~p", [Request]),
+ %% Check if key valid
+ Key = xml:get_attr_s("key", Attrs),
+ NewKey = xml:get_attr_s("newkey", Attrs),
+ KeyAllow =
+ case RidAllow of
+ repeat ->
+ true;
+ false ->
+ false;
+ {true, _} ->
+ case StateData#state.key of
+ "" ->
+ true;
+ OldKey ->
+ NextKey = sha:sha(Key),
+ ?DEBUG("Key/OldKey/NextKey: ~s/~s/~s", [Key, OldKey, NextKey]),
+ if
+ OldKey == NextKey ->
+ true;
+ true ->
+ ?DEBUG("wrong key: ~s",[Key]),
+ false
+ end
+ end
+ end,
+ TNow = tnow(),
+ LastPoll = if
+ Payload == [] ->
+ TNow;
+ true ->
+ 0
+ end,
+ if
+ (Payload == []) and
+ (Hold == 0) and
+ (TNow - StateData#state.last_poll < ?MIN_POLLING) ->
+ Reply = {error, polling_too_frequently},
+ {reply, Reply, StateName, StateData};
+ KeyAllow ->
+ case RidAllow of
+ false ->
+ Reply = {error, not_exists},
+ {reply, Reply, StateName, StateData};
+ repeat ->
+ ?DEBUG("REPEATING ~p", [Rid]),
+ Reply = case [El#hbr.out ||
+ El <- StateData#state.req_list,
+ El#hbr.rid == Rid] of
+ [] ->
+ {error, not_exists};
+ [Out | _XS] ->
+ {repeat, lists:reverse(Out)}
+ end,
+ {reply, Reply, StateName, StateData#state{input = cancel,
+ last_poll = LastPoll}};
+ {true, Pause} ->
+ SaveKey = if
+ NewKey == "" ->
+ Key;
+ true ->
+ NewKey
+ end,
+ ?DEBUG(" -- SaveKey: ~s~n", [SaveKey]),
+
+ %% save request
+ ReqList1 =
+ [El || El <- StateData#state.req_list,
+ El#hbr.rid > (Rid - 1 - Hold)],
+ ReqList =
+ case lists:keymember(Rid, #hbr.rid, ReqList1) of
+ true ->
+ ReqList1;
+ false ->
+ [#hbr{rid = Rid,
+ key = StateData#state.key,
+ out = []
+ } |
+ ReqList1
+ ]
+ end,
+ ?DEBUG("reqlist: ~p", [ReqList]),
+
+ %% setup next timer
+ cancel_timer(StateData#state.timer),
+ Timer = set_inactivity_timer(Pause,
+ StateData#state.max_inactivity),
+ case StateData#state.waiting_input of
+ false ->
+ Input =
+ lists:foldl(
+ fun queue:in/2,
+ StateData#state.input, Payload),
+ Reply = ok,
+ process_buffered_request(Reply, StateName,
+ StateData#state{input = Input,
+ rid = Rid,
+ key = SaveKey,
+ ctime = TNow,
+ timer = Timer,
+ pause = Pause,
+ last_poll = LastPoll,
+ req_list = ReqList,
+ ip = IP
+ });
+ C2SPid ->
+ case StreamTo of
+ {To, ""} ->
+ gen_fsm:send_event(
+ C2SPid,
+ {xmlstreamstart, "stream:stream",
+ [{"to", To},
+ {"xmlns", ?NS_CLIENT},
+ {"xmlns:stream", ?NS_STREAM}]});
+ {To, Version} ->
+ gen_fsm:send_event(
+ C2SPid,
+ {xmlstreamstart, "stream:stream",
+ [{"to", To},
+ {"xmlns", ?NS_CLIENT},
+ {"version", Version},
+ {"xmlns:stream", ?NS_STREAM}]});
+ _ ->
+ ok
+ end,
+
+ MaxInactivity = get_max_inactivity(StreamTo, StateData#state.max_inactivity),
+ MaxPause = get_max_inactivity(StreamTo, StateData#state.max_pause),
+
+ ?DEBUG("really sending now: ~p", [Payload]),
+ lists:foreach(
+ fun({xmlstreamend, End}) ->
+ gen_fsm:send_event(
+ C2SPid, {xmlstreamend, End});
+ (El) ->
+ gen_fsm:send_event(
+ C2SPid, {xmlstreamelement, El})
+ end, Payload),
+ Reply = ok,
+ process_buffered_request(Reply, StateName,
+ StateData#state{input = queue:new(),
+ rid = Rid,
+ key = SaveKey,
+ ctime = TNow,
+ timer = Timer,
+ pause = Pause,
+ last_poll = LastPoll,
+ req_list = ReqList,
+ max_inactivity = MaxInactivity,
+ max_pause = MaxPause,
+ ip = IP
+ })
+ end
+ end;
+ true ->
+ Reply = {error, bad_key},
+ {reply, Reply, StateName, StateData}
+ end.
+
+process_buffered_request(Reply, StateName, StateData) ->
+ Rid = StateData#state.rid,
+ Requests = StateData#state.unprocessed_req_list,
+ case lists:keysearch(Rid+1, 2, Requests) of
+ {value, Request} ->
+ ?DEBUG("Processing buffered request: ~p", [Request]),
+ NewRequests = lists:keydelete(Rid+1, 2, Requests),
+ handle_http_put_event(
+ Request, StateName,
+ StateData#state{unprocessed_req_list = NewRequests});
+ _ ->
+ {reply, Reply, StateName, StateData, hibernate}
+ end.
+
+handle_http_put(Sid, Rid, Attrs, Payload, PayloadSize, StreamStart, IP) ->
+ case http_put(Sid, Rid, Attrs, Payload, PayloadSize, StreamStart, IP) of
+ {error, not_exists} ->
+ ?DEBUG("no session associated with sid: ~p", [Sid]),
+ {404, ?HEADER, ""};
+ {{error, Reason}, Sess} ->
+ ?DEBUG("Error on HTTP put. Reason: ~p", [Reason]),
+ handle_http_put_error(Reason, Sess);
+ {{repeat, OutPacket}, Sess} ->
+ ?DEBUG("http_put said \"repeat!\" ...~nOutPacket: ~p", [OutPacket]),
+ send_outpacket(Sess, OutPacket);
+ {{wait, Pause}, _Sess} ->
+ ?DEBUG("Trafic Shaper: Delaying request ~p", [Rid]),
+ timer:sleep(Pause),
+ %{200, ?HEADER,
+ % xmpp_json:to_json(
+ % {xmlelement, "body",
+ % [{"xmlns", ?NS_HTTP_BIND},
+ % {"type", "error"}], []})};
+ handle_http_put(Sid, Rid, Attrs, Payload, PayloadSize,
+ StreamStart, IP);
+ {buffered, _Sess} ->
+ {200, ?HEADER, "{\"body\":{ \"xmlns\":\""++?NS_HTTP_BIND++"\"}}"};
+ {ok, Sess} ->
+ prepare_response(Sess, Rid, [], StreamStart)
+ end.
+
+http_put(Sid, Rid, Attrs, Payload, PayloadSize, StreamStart, IP) ->
+ ?DEBUG("Looking for session: ~p", [Sid]),
+ case get_session(Sid) of
+ {error, _} ->
+ {error, not_exists};
+ {ok, #http_bind{pid = FsmRef, hold=Hold, to={To, StreamVersion}}=Sess}->
+ NewStream =
+ case StreamStart of
+ true ->
+ {To, StreamVersion};
+ _ ->
+ ""
+ end,
+ {gen_fsm:sync_send_all_state_event(
+ FsmRef, #http_put{rid = Rid, attrs = Attrs, payload = Payload,
+ payload_size = PayloadSize, hold = Hold,
+ stream = NewStream, ip = IP}, 30000), Sess}
+ end.
+
+handle_http_put_error(Reason, #http_bind{pid=FsmRef, version=Version})
+ when Version >= 0 ->
+ gen_fsm:sync_send_all_state_event(FsmRef, {stop, {put_error,Reason}}),
+ case Reason of
+ not_exists ->
+ {200, ?HEADER,
+ mochijson2:encode(
+ xmpp_json:to_json(
+ {xmlelement, "body",
+ [{"xmlns", ?NS_HTTP_BIND},
+ {"type", "terminate"},
+ {"condition", "item-not-found"}], []}))};
+ bad_key ->
+ {200, ?HEADER,
+ mochijson2:encode(
+ xmpp_json:to_json(
+ {xmlelement, "body",
+ [{"xmlns", ?NS_HTTP_BIND},
+ {"type", "terminate"},
+ {"condition", "item-not-found"}], []}))};
+ polling_too_frequently ->
+ {200, ?HEADER,
+ mochijson2:encode(
+ xmpp_json:to_json(
+ {xmlelement, "body",
+ [{"xmlns", ?NS_HTTP_BIND},
+ {"type", "terminate"},
+ {"condition", "policy-violation"}], []}))}
+ end;
+handle_http_put_error(Reason, #http_bind{pid=FsmRef}) ->
+ gen_fsm:sync_send_all_state_event(FsmRef,{stop, {put_error_no_version, Reason}}),
+ case Reason of
+ not_exists -> %% bad rid
+ ?DEBUG("Closing HTTP bind session (Bad rid).", []),
+ {404, ?HEADER, ""};
+ bad_key ->
+ ?DEBUG("Closing HTTP bind session (Bad key).", []),
+ {404, ?HEADER, ""};
+ polling_too_frequently ->
+ ?DEBUG("Closing HTTP bind session (User polling too frequently).", []),
+ {403, ?HEADER, ""}
+ end.
+
+%% Control RID ordering
+rid_allow(none, _NewRid, _Attrs, _Hold, _MaxPause) ->
+ %% First request - nothing saved so far
+ {true, 0};
+rid_allow(OldRid, NewRid, Attrs, Hold, MaxPause) ->
+ ?DEBUG("Previous rid / New rid: ~p/~p", [OldRid, NewRid]),
+ if
+ %% We did not miss any packet, we can process it immediately:
+ NewRid == OldRid + 1 ->
+ case catch list_to_integer(
+ xml:get_attr_s("pause", Attrs)) of
+ {'EXIT', _} ->
+ {true, 0};
+ Pause1 when Pause1 =< MaxPause ->
+ ?DEBUG("got pause: ~p", [Pause1]),
+ {true, Pause1};
+ _ ->
+ {true, 0}
+ end;
+ %% We have missed packets, we need to cached it to process it later on:
+ (OldRid < NewRid) and
+ (NewRid =< (OldRid + Hold + 1)) ->
+ buffer;
+ (NewRid =< OldRid) and
+ (NewRid > OldRid - Hold - 1) ->
+ repeat;
+ true ->
+ false
+ end.
+
+update_shaper(ShaperState, PayloadSize) ->
+ {NewShaperState, Pause} = shaper:update(ShaperState, PayloadSize),
+ if
+ Pause > 0 ->
+ ShaperTimer = erlang:start_timer(Pause, self(), activate), %% MR: Seems timer is not needed. Activate is not handled
+ {NewShaperState, ShaperTimer};
+ true ->
+ {NewShaperState, undefined}
+ end.
+
+prepare_response(Sess, Rid, OutputEls, StreamStart) ->
+ receive after Sess#http_bind.process_delay -> ok end,
+ case catch http_get(Sess, Rid) of
+ {ok, cancel} ->
+ %% actually it would be better if we could completely
+ %% cancel this request, but then we would have to hack
+ %% ejabberd_http and I'm too lazy now
+ {200, ?HEADER, "{\"body\": {\"type\":\"error\", \"xmlns\":\""++?NS_HTTP_BIND++"\"/>"};
+ {ok, empty} ->
+ {200, ?HEADER, "{\"body\":{ \"xmlns\":\""++?NS_HTTP_BIND++"\"}}"};
+ {ok, terminate} ->
+ {200, ?HEADER, "{\"body\": {\"type\":\"terminate\", \"xmlns\":\""++?NS_HTTP_BIND++"\"/>"};
+ {ok, ROutPacket} ->
+ OutPacket = lists:reverse(ROutPacket),
+ ?DEBUG("OutPacket: ~p", [OutputEls++OutPacket]),
+ prepare_outpacket_response(Sess, Rid, OutputEls++OutPacket, StreamStart);
+ {'EXIT', {shutdown, _}} ->
+ {200, ?HEADER, "{\"body\": {\"type\":\"terminate\",\"condition\":\"system-shutdown\", \"xmlns\":\""++ ?NS_HTTP_BIND++"\"}}"};
+ {'EXIT', _Reason} ->
+ {200, ?HEADER, "{\"body\": {\"type\":\"terminate, \"xmlns\":\""++ ?NS_HTTP_BIND++"\"}}"}
+ end.
+
+%% Send output payloads on establised sessions
+prepare_outpacket_response(Sess, _Rid, OutPacket, false) ->
+ case catch send_outpacket(Sess, OutPacket) of
+ {'EXIT', _Reason} ->
+ {200, ?HEADER,
+ "{\"body\": {\"type\":\"terminate\", \"xmlns\":\""++ ?NS_HTTP_BIND++"\"}}"};
+ SendRes ->
+ SendRes
+ end;
+%% Handle a new session along with its output payload
+prepare_outpacket_response(#http_bind{id=Sid, wait=Wait,
+ hold=Hold, to=To}=Sess,
+ Rid, OutPacket, true) ->
+ case OutPacket of
+ [{xmlstreamstart, _, OutAttrs} | Els] ->
+ AuthID = xml:get_attr_s("id", OutAttrs),
+ From = xml:get_attr_s("from", OutAttrs),
+ Version = xml:get_attr_s("version", OutAttrs),
+ OutEls =
+ case Els of
+ [] ->
+ [];
+ [{xmlstreamelement,
+ {xmlelement, "stream:features",
+ StreamAttribs, StreamEls}}
+ | StreamTail] ->
+ TypedTail =
+ [check_default_xmlns(OEl) ||
+ {xmlstreamelement, OEl} <-
+ StreamTail],
+ [{xmlelement, "stream:features",
+ [{"xmlns:stream",
+ ?NS_STREAM}] ++
+ StreamAttribs, StreamEls}] ++
+ TypedTail;
+ StreamTail ->
+ [check_default_xmlns(OEl) ||
+ {xmlstreamelement, OEl} <-
+ StreamTail]
+ end,
+ case OutEls of
+ [] ->
+ prepare_response(Sess, Rid, OutPacket, true);
+ [{xmlelement,
+ "stream:error",_,_}] ->
+ {200, ?HEADER, "{\"body\" : {\"type\":\"terminate\", "
+ "\"condition\":\"host-unknown\", "
+ "\"xmlns\"=\""++?NS_HTTP_BIND++"\"}}"};
+ _ ->
+ BOSH_attribs =
+ [{"authid", AuthID},
+ {"xmlns:xmpp", ?NS_BOSH},
+ {"xmlns:stream", ?NS_STREAM}] ++
+ case OutEls of
+ [] ->
+ [];
+ _ ->
+ [{"xmpp:version", Version}]
+ end,
+ MaxInactivity = get_max_inactivity(To, ?MAX_INACTIVITY),
+ MaxPause = get_max_pause(To),
+ {200, ?HEADER,
+ mochijson2:encode(
+ xmpp_json:to_json(
+ {xmlelement,"body",
+ [{"xmlns",
+ ?NS_HTTP_BIND},
+ {"sid", Sid},
+ {"wait", integer_to_list(Wait)},
+ {"requests", integer_to_list(Hold+1)},
+ {"inactivity",
+ integer_to_list(
+ trunc(MaxInactivity/1000))},
+ {"maxpause",
+ integer_to_list(MaxPause)},
+ {"polling",
+ integer_to_list(
+ trunc(?MIN_POLLING/1000000))},
+ {"ver", ?BOSH_VERSION},
+ {"from", From},
+ {"secure", "true"} %% we're always being secure
+ ] ++ BOSH_attribs,OutEls}))}
+ end;
+ _ ->
+ {200, ?HEADER, "{\"body\" : {\"type\":\"terminate\", "
+ "\"condition\":\"internal-server-error\", "
+ "\"xmlns\"=\""++?NS_HTTP_BIND++"\"}}"}
+ end.
+
+
+http_get(#http_bind{pid = FsmRef, wait = Wait, hold = Hold}, Rid) ->
+ gen_fsm:sync_send_all_state_event(
+ FsmRef, {http_get, Rid, Wait, Hold}, 2 * ?MAX_WAIT * 1000).
+
+send_outpacket(#http_bind{pid = FsmRef}, OutPacket) ->
+ case OutPacket of
+ [] ->
+ {200, ?HEADER, "{\"body\": {\"xmlns\":\""++?NS_HTTP_BIND++"\"}}"};
+ [{xmlstreamend, _}] ->
+ gen_fsm:sync_send_all_state_event(FsmRef,{stop,stream_closed}),
+ {200, ?HEADER, "{\"body\": {\"xmlns\":"++?NS_HTTP_BIND++"\"}}"};
+ _ ->
+ %% TODO: We parse to add a default namespace to packet,
+ %% The spec says adding the jabber:client namespace if
+ %% mandatory, even if some implementation do not do that
+ %% change on packets.
+ %% I think this should be an option to avoid modifying
+ %% packet in most case.
+ AllElements =
+ lists:all(fun({xmlstreamelement,
+ {xmlelement, "stream:error", _, _}}) -> false;
+ ({xmlstreamelement, _}) -> true;
+ (_) -> false
+ end, OutPacket),
+ case AllElements of
+ true ->
+ TypedEls = [check_default_xmlns(OEl) ||
+ {xmlstreamelement, OEl} <- OutPacket],
+ Body = mochijson2:encode(xmpp_json:to_json(
+ {xmlelement,"body",
+ [{"xmlns",
+ ?NS_HTTP_BIND}],
+ TypedEls})),
+ ?DEBUG(" --- outgoing data --- ~n~s~n --- END --- ~n",
+ [Body]),
+ {200, ?HEADER, Body};
+ false ->
+ case OutPacket of
+ [{xmlstreamstart, _, _} | SEls] ->
+ OutEls =
+ case SEls of
+ [{xmlstreamelement,
+ {xmlelement,
+ "stream:features",
+ StreamAttribs, StreamEls}} |
+ StreamTail] ->
+ TypedTail =
+ [check_default_xmlns(OEl) ||
+ {xmlstreamelement, OEl} <-
+ StreamTail],
+ [{xmlelement,
+ "stream:features",
+ [{"xmlns:stream",
+ ?NS_STREAM}] ++
+ StreamAttribs, StreamEls}] ++
+ TypedTail;
+ StreamTail ->
+ [check_default_xmlns(OEl) ||
+ {xmlstreamelement, OEl} <-
+ StreamTail]
+ end,
+ {200, ?HEADER,
+ mochijson2:encode(
+ xmpp_json:to_json(
+ {xmlelement,"body",
+ [{"xmlns",
+ ?NS_HTTP_BIND}],
+ OutEls}))};
+ _ ->
+ SErrCond =
+ lists:filter(
+ fun({xmlstreamelement,
+ {xmlelement, "stream:error",
+ _, _}}) ->
+ true;
+ (_) -> false
+ end, OutPacket),
+ StreamErrCond =
+ case SErrCond of
+ [] ->
+ null;
+ [{xmlstreamelement,
+ {xmlelement, _, _, _Cond} =
+ StreamErrorTag} | _] ->
+ [StreamErrorTag]
+ end,
+ gen_fsm:sync_send_all_state_event(FsmRef,
+ {stop, {stream_error,OutPacket}}),
+ case StreamErrCond of
+ null ->
+ {200, ?HEADER,
+ "{\"body\" : {\"\"type\"\":\"terminate\", "
+ "\"condition\":\"internal-server-error\", "
+ "\"xmlns\"=\""++?NS_HTTP_BIND++"\"}}"};
+ _ ->
+ {200, ?HEADER,
+ "{\"body\" : {\"\"type\"\":\"terminate\", "
+ "\"condition\":\"remote-stream-error\", "
+ "\"xmlns\":\""++?NS_HTTP_BIND++"\", " ++
+ "\"xmlns:stream\":\""++?NS_STREAM++"\" \"$\":" ++
+ elements_to_string(StreamErrCond) ++
+ "}}"}
+ end
+ end
+ end
+ end.
+
+parse_request(Data, PayloadSize, MaxStanzaSize) ->
+ ?DEBUG("--- incoming data --- ~n~p~n --- END --- ", [xmpp_json:from_json(mochijson2:decode(Data))]),
+ %% MR: I do not think it works if put put several elements in the
+ %% same body:
+ case xmpp_json:from_json(mochijson2:decode(Data)) of
+ {xmlstreamelement,{xmlelement, "body", Attrs, Els}} ->
+ Xmlns = xml:get_attr_s("xmlns",Attrs),
+ if
+ Xmlns /= ?NS_HTTP_BIND ->
+ {error, bad_request};
+ true ->
+ case catch list_to_integer(xml:get_attr_s("rid", Attrs)) of
+ {'EXIT', _} ->
+ {error, bad_request};
+ Rid ->
+ %% I guess this is to remove XMLCDATA: Is it really needed ?
+ FixedEls =
+ lists:filter(
+ fun(I) ->
+ case I of
+ {xmlelement, _, _, _} ->
+ true;
+ _ ->
+ false
+ end
+ end, Els),
+ Sid = xml:get_attr_s("sid",Attrs),
+ if
+ PayloadSize =< MaxStanzaSize ->
+ {ok, {Sid, Rid, Attrs, FixedEls}};
+ true ->
+ {size_limit, Sid}
+ end
+ end
+ end;
+ {xmlstreamelement,{xmlelement, _Name, _Attrs, _Els}} ->
+ {error, bad_request};
+ {error, _Reason} ->
+ {error, bad_request}
+ end.
+
+send_receiver_reply(undefined, _Reply) ->
+ ok;
+send_receiver_reply(Receiver, Reply) ->
+ gen_fsm:reply(Receiver, Reply).
+
+
+%% Cancel timer and empty message queue.
+cancel_timer(undefined) ->
+ ok;
+cancel_timer(Timer) ->
+ erlang:cancel_timer(Timer),
+ receive
+ {timeout, Timer, _} ->
+ ok
+ after 0 ->
+ ok
+ end.
+
+%% If client asked for a pause (pause > 0), we apply the pause value
+%% as inactivity timer:
+set_inactivity_timer(Pause, _MaxInactivity) when Pause > 0 ->
+ erlang:start_timer(Pause*1000, self(), []);
+%% Otherwise, we apply the max_inactivity value as inactivity timer:
+set_inactivity_timer(_Pause, MaxInactivity) ->
+ erlang:start_timer(MaxInactivity, self(), []).
+
+
+%% TODO: Use tail recursion and list reverse ?
+elements_to_string([]) ->
+ [];
+elements_to_string([El | Els]) ->
+ [mochijson2:encode(xmpp_json:to_json(El))|elements_to_string(Els)].
+
+%% @spec (To, Default::integer()) -> integer()
+%% where To = [] | {Host::string(), Version::string()}
+get_max_inactivity({Host, _}, Default) ->
+ case gen_mod:get_module_opt(Host, mod_http_bind, max_inactivity, undefined) of
+ Seconds when is_integer(Seconds) ->
+ Seconds * 1000;
+ undefined ->
+ Default
+ end;
+get_max_inactivity(_, Default) ->
+ Default.
+
+get_max_pause({Host, _}) ->
+ gen_mod:get_module_opt(Host, mod_http_bind, max_pause, ?MAX_PAUSE);
+get_max_pause(_) ->
+ ?MAX_PAUSE.
+
+%% Current time as integer
+tnow() ->
+ {TMegSec, TSec, TMSec} = now(),
+ (TMegSec * 1000000 + TSec) * 1000000 + TMSec.
+
+check_default_xmlns({xmlelement, Name, Attrs, Els} = El) ->
+ case xml:get_tag_attr_s("xmlns", El) of
+ "" -> {xmlelement, Name, [{"xmlns", ?NS_CLIENT} | Attrs], Els};
+ _ -> El
+ end.
+
+%% Check that mod_http_bind has been defined in config file.
+%% Print a warning in log file if this is not the case.
+check_bind_module(XmppDomain) ->
+ case gen_mod:is_loaded(XmppDomain, mod_http_bind) of
+ true -> true;
+ false -> ?ERROR_MSG("You are trying to use BOSH (HTTP Bind), but the module mod_http_bind is not started.~n"
+ "Check your 'modules' section in your ejabberd configuration file.",[]),
+ false
+ end.
+
+make_sid() ->
+ sha:sha(term_to_binary({now(), make_ref()}))
+ ++ "-" ++ ejabberd_cluster:node_id().
+
+get_session(SID) ->
+ case string:tokens(SID, "-") of
+ [_, NodeID] ->
+ case ejabberd_cluster:get_node_by_id(NodeID) of
+ Node when Node == node() ->
+ case mnesia:dirty_read({http_bind, SID}) of
+ [] ->
+ {error, enoent};
+ [Session] ->
+ {ok, Session}
+ end;
+ Node ->
+ case catch rpc:call(Node, mnesia, dirty_read,
+ [{http_bind, SID}], 5000) of
+ [Session] ->
+ {ok, Session};
+ _ ->
+ {error, enoent}
+ end
+ end;
+ _ ->
+ {error, enoent}
+ end.
diff --git a/src/web/ejabberd_http_poll.erl b/src/web/ejabberd_http_poll.erl
index 8c80ddec8..cde8124f9 100644
--- a/src/web/ejabberd_http_poll.erl
+++ b/src/web/ejabberd_http_poll.erl
@@ -77,9 +77,12 @@
%%% API
%%%----------------------------------------------------------------------
start(ID, Key, IP) ->
+ update_tables(),
mnesia:create_table(http_poll,
[{ram_copies, [node()]},
+ {local_content, true},
{attributes, record_info(fields, http_poll)}]),
+ mnesia:add_table_copy(http_poll, node(), ram_copies),
supervisor:start_child(ejabberd_http_poll_sup, [ID, Key, IP]).
start_link(ID, Key, IP) ->
@@ -115,9 +118,9 @@ process([], #request{data = Data,
{ok, ID1, Key, NewKey, Packet} ->
ID = if
(ID1 == "0") or (ID1 == "mobile") ->
- NewID = sha:sha(term_to_binary({now(), make_ref()})),
+ NewID = make_sid(),
{ok, Pid} = start(NewID, "", IP),
- mnesia:transaction(
+ mnesia:async_dirty(
fun() ->
mnesia:write(#http_poll{id = NewID,
pid = Pid})
@@ -356,7 +359,7 @@ handle_info(_, StateName, StateData) ->
%% Returns: any
%%----------------------------------------------------------------------
terminate(_Reason, _StateName, StateData) ->
- mnesia:transaction(
+ mnesia:async_dirty(
fun() ->
mnesia:delete({http_poll, StateData#state.id})
end),
@@ -381,19 +384,19 @@ terminate(_Reason, _StateName, StateData) ->
%%%----------------------------------------------------------------------
http_put(ID, Key, NewKey, Packet) ->
- case mnesia:dirty_read({http_poll, ID}) of
- [] ->
+ case get_session(ID) of
+ {error, _} ->
{error, not_exists};
- [#http_poll{pid = FsmRef}] ->
+ {ok, #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
- [] ->
+ case get_session(ID) of
+ {error, _} ->
{error, not_exists};
- [#http_poll{pid = FsmRef}] ->
+ {ok, #http_poll{pid = FsmRef}} ->
gen_fsm:sync_send_all_state_event(FsmRef, http_get)
end.
@@ -452,3 +455,39 @@ get_jid(Type, ParsedPacket) ->
false ->
jlib:make_jid("","","")
end.
+
+update_tables() ->
+ case catch mnesia:table_info(http_poll, local_content) of
+ false ->
+ mnesia:delete_table(http_poll);
+ _ ->
+ ok
+ end.
+
+make_sid() ->
+ sha:sha(term_to_binary({now(), make_ref()}))
+ ++ "-" ++ ejabberd_cluster:node_id().
+
+get_session(SID) ->
+ case string:tokens(SID, "-") of
+ [_, NodeID] ->
+ case ejabberd_cluster:get_node_by_id(NodeID) of
+ Node when Node == node() ->
+ case mnesia:dirty_read({http_poll, SID}) of
+ [] ->
+ {error, enoent};
+ [Session] ->
+ {ok, Session}
+ end;
+ Node ->
+ case catch rpc:call(Node, mnesia, dirty_read,
+ [{http_poll, SID}], 5000) of
+ [Session] ->
+ {ok, Session};
+ _ ->
+ {error, enoent}
+ end
+ end;
+ _ ->
+ {error, enoent}
+ end.
diff --git a/src/web/ejabberd_http_ws.erl b/src/web/ejabberd_http_ws.erl
new file mode 100644
index 000000000..f79c41ce1
--- /dev/null
+++ b/src/web/ejabberd_http_ws.erl
@@ -0,0 +1,214 @@
+%%%----------------------------------------------------------------------
+%%% 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-2010 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 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/2,
+ setopts/2,
+ sockname/1, peername/1,
+ controlling_process/2,
+ become_controller/2,
+ close/1]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("ejabberd_http.hrl").
+
+-record(state, {
+ socket,
+ timeout,
+ timer,
+ input = "",
+ waiting_input = false, %% {ReceiverPid, Tag}
+ last_receiver,
+ ws}).
+
+%-define(DBGFSM, true).
+
+-ifdef(DBGFSM).
+-define(FSMOPTS, [{debug, [trace]}]).
+-else.
+-define(FSMOPTS, []).
+-endif.
+
+-define(WEBSOCKET_TIMEOUT, 300000).
+%
+%
+%%%%----------------------------------------------------------------------
+%%%% API
+%%%%----------------------------------------------------------------------
+start(WS) ->
+ supervisor:start_child(ejabberd_wsloop_sup, [WS]).
+
+start_link(WS) ->
+ gen_fsm:start_link(?MODULE, [WS],?FSMOPTS).
+
+send({http_ws, FsmRef, _IP}, Packet) ->
+ gen_fsm:sync_send_all_state_event(FsmRef, {send, 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).
+
+%%% Internal
+
+
+init([WS]) ->
+ %% Read c2s options from the first ejabberd_c2s configuration in
+ %% the config file listen section
+ %% TODO: We should have different access and shaper values for
+ %% each connector. The default behaviour should be however to use
+ %% the default c2s restrictions if not defined for the current
+ %% connector.
+ Opts = ejabberd_c2s_config:get_c2s_limits(),
+
+ WSTimeout = case ejabberd_config:get_local_option({websocket_timeout,
+ ?MYNAME}) of
+ %% convert seconds of option into milliseconds
+ Int when is_integer(Int) -> Int*1000;
+ undefined -> ?WEBSOCKET_TIMEOUT
+ end,
+
+ Socket = {http_ws, self(), WS:get(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}}.
+
+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, list_to_binary(Input)},
+ {next_state, StateName, StateData#state{input = "",
+ waiting_input = false,
+ last_receiver = Receiver
+ }}
+ end.
+
+handle_sync_event({send, Packet}, _From, StateName, #state{ws = WS} = StateData) ->
+ Packet2 = if
+ is_binary(Packet) ->
+ Packet;
+ true ->
+ list_to_binary(Packet)
+ end,
+ %?DEBUG("sending on websocket : ~p ", [Packet2]),
+ WS:send(Packet2),
+ {reply, ok, StateName, StateData};
+
+handle_sync_event(close, From, _StateName, StateData)->
+ {stop, normal, StateData}.
+
+handle_info(closed, _StateName, StateData) ->
+ {stop, normal, StateData};
+
+handle_info({browser, Packet}, StateName, StateData)->
+ %?DEBUG("Received on websocket : ~p ", [Packet]),
+ NPacket = unicode:characters_to_binary(Packet,latin1),
+ NewState = case StateData#state.waiting_input of
+ false ->
+ Input = [StateData#state.input|NPacket],
+ StateData#state{input = Input};
+ {Receiver, _Tag} ->
+ Receiver ! {tcp, StateData#state.socket,NPacket},
+ cancel_timer(StateData#state.timer),
+ Timer = erlang:start_timer(StateData#state.timeout, self(), []),
+ StateData#state{waiting_input = false,
+ last_receiver = Receiver,
+ timer = Timer}
+ end,
+ {next_state, StateName, NewState};
+
+
+handle_info({timeout, Timer, _}, _StateName,
+ #state{timer = Timer} = StateData) ->
+ {stop, normal, StateData};
+
+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.
+
+cancel_timer(Timer) ->
+ erlang:cancel_timer(Timer),
+ receive
+ {timeout, Timer, _} ->
+ ok
+ after 0 ->
+ ok
+ end. \ No newline at end of file
diff --git a/src/web/ejabberd_http_wsjson.erl b/src/web/ejabberd_http_wsjson.erl
new file mode 100644
index 000000000..2abd12b3b
--- /dev/null
+++ b/src/web/ejabberd_http_wsjson.erl
@@ -0,0 +1,219 @@
+%%%----------------------------------------------------------------------
+%%% File : ejabberd_websocket.erl
+%%% Author : Eric Cestari <ecestari@process-one.net>
+%%% Purpose : JSON - XMPP Websocket module support
+%%% Created : 09-10-2010 by Eric Cestari <ecestari@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2010 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+-module (ejabberd_http_wsjson).
+-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]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("ejabberd_http.hrl").
+
+-record(state, {
+ socket,
+ timeout,
+ timer,
+ input = [],
+ waiting_input = false, %% {ReceiverPid, Tag}
+ last_receiver,
+ ws}).
+
+%-define(DBGFSM, true).
+
+-ifdef(DBGFSM).
+-define(FSMOPTS, [{debug, [trace]}]).
+-else.
+-define(FSMOPTS, []).
+-endif.
+
+-define(WEBSOCKET_TIMEOUT, 300000).
+%
+%
+%%%%----------------------------------------------------------------------
+%%%% API
+%%%%----------------------------------------------------------------------
+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, 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).
+
+%%% Internal
+
+
+init([WS]) ->
+ %% Read c2s options from the first ejabberd_c2s configuration in
+ %% the config file listen section
+ %% TODO: We should have different access and shaper values for
+ %% each connector. The default behaviour should be however to use
+ %% the default c2s restrictions if not defined for the current
+ %% connector.
+ Opts = [{xml_socket, true}|ejabberd_c2s_config:get_c2s_limits()],
+
+ WSTimeout = case ejabberd_config:get_local_option({websocket_timeout,
+ ?MYNAME}) of
+ %% convert seconds of option into milliseconds
+ Int when is_integer(Int) -> Int*1000;
+ undefined -> ?WEBSOCKET_TIMEOUT
+ end,
+
+ Socket = {http_ws, self(), WS:get(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}}.
+
+handle_event({activate, From}, StateName, StateData) ->
+ case StateData#state.input of
+ [] ->
+ {next_state, StateName,
+ StateData#state{waiting_input = {From, ok}}};
+ Input ->
+ Receiver = From,
+ lists:reverse(lists:map(fun(Packet)->
+ Receiver ! {tcp, StateData#state.socket, [Packet]}
+ end, Input)),
+ {next_state, StateName, StateData#state{input = "",
+ waiting_input = false,
+ last_receiver = Receiver
+ }}
+ end.
+
+handle_sync_event({send, Packet}, _From, StateName, #state{ws = WS} = StateData) ->
+ EJson = xmpp_json:to_json(Packet),
+ Json = mochijson2:encode(EJson),
+ WS:send(iolist_to_binary(Json)),
+ {reply, ok, StateName, StateData};
+
+handle_sync_event(close, _From, _StateName, StateData) ->
+ Reply = ok,
+ {stop, normal, Reply, StateData}.
+
+handle_info({browser, <<"\n">>}, StateName, StateData)->
+ NewState = case StateData#state.waiting_input of
+ false ->
+ ok;
+ {Receiver, _Tag} ->
+ Receiver ! {tcp, StateData#state.socket,<<"\n">>},
+ cancel_timer(StateData#state.timer),
+ Timer = erlang:start_timer(StateData#state.timeout, self(), []),
+ StateData#state{waiting_input = false,
+ last_receiver = Receiver,
+ timer = Timer}
+ end,
+ {next_state, StateName, NewState};
+handle_info({browser, JsonPacket}, StateName, StateData)->
+ NewState = case StateData#state.waiting_input of
+ false ->
+ EJson = mochijson2:decode(JsonPacket),
+ Packet = xmpp_json:from_json(EJson),
+ Input = [Packet | StateData#state.input],
+ StateData#state{input = Input};
+ {Receiver, _Tag} ->
+ %?DEBUG("Received from browser : ~p", [JsonPacket]),
+ EJson = mochijson2:decode(JsonPacket),
+ %?DEBUG("decoded : ~p", [EJson]),
+ Packet = xmpp_json:from_json(EJson),
+ %?DEBUG("sending to c2s : ~p", [Packet]),
+ Receiver ! {tcp, StateData#state.socket,[Packet]},
+ cancel_timer(StateData#state.timer),
+ Timer = erlang:start_timer(StateData#state.timeout, self(), []),
+ StateData#state{waiting_input = false,
+ last_receiver = Receiver,
+ timer = Timer}
+ end,
+ {next_state, StateName, NewState};
+
+
+handle_info({timeout, Timer, _}, _StateName,
+ #state{timer = Timer} = StateData) ->
+ {stop, normal, StateData};
+
+handle_info(_, StateName, StateData) ->
+ {next_state, StateName, StateData}.
+
+
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+ {ok, StateName, StateData}.
+
+terminate(_Reason, _StateName, _StateData) -> ok.
+
+cancel_timer(Timer) ->
+ erlang:cancel_timer(Timer),
+ receive
+ {timeout, Timer, _} ->
+ ok
+ after 0 ->
+ ok
+ end.
diff --git a/src/web/ejabberd_websocket.erl b/src/web/ejabberd_websocket.erl
new file mode 100644
index 000000000..8030842a0
--- /dev/null
+++ b/src/web/ejabberd_websocket.erl
@@ -0,0 +1,435 @@
+%%%----------------------------------------------------------------------
+%%% 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-2010 ProcessOne
+%%%----------------------------------------------------------------------
+
+-module (ejabberd_websocket).
+-author('ecestari@process-one.net').
+-export([connect/2, check/2, is_acceptable/1]).
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("ejabberd_http.hrl").
+
+check(_Path, Headers)->
+ % set supported websocket protocols, order does matter
+ VsnSupported = [{'draft-hybi', 8}, {'draft-hybi', 13}, {'draft-hixie', 0}, {'draft-hixie', 68}],
+ % checks
+ check_websockets(VsnSupported, Headers).
+
+% Checks if websocket can be access by client
+% If origins are set in configuration, check if it belongs
+% If origins not set, access is open.
+is_acceptable(#ws{origin=Origin, protocol=Protocol,
+ headers = Headers, acceptable_origins = Origins, auth_module=undefined})->
+ ClientProtocol = lists:keyfind("Sec-Websocket-Protocol",1, Headers),
+ case {(Origins == []) or lists:member(Origin, Origins), ClientProtocol, Protocol } of
+ {false, _, _} ->
+ ?INFO_MSG("client does not come from authorized origin", []),
+ false;
+ {_, false, _} ->
+ true;
+ {_, {_, P}, P} ->
+ true;
+ _ = E->
+ ?INFO_MSG("Wrong protocol requested : ~p", [E]),
+ false
+ end;
+is_acceptable(#ws{local_path=LocalPath, origin=Origin, ip=IP, q=Q, protocol=Protocol, headers = Headers,auth_module=Module})->
+ Module:is_acceptable(LocalPath, Q, Origin, Protocol, IP, Headers).
+
+% Connect and handshake with Websocket.
+connect(#ws{vsn = Vsn, socket = Socket, q=Q,origin=Origin, host=Host, port=Port, sockmod = SockMod, path = Path, headers = Headers, ws_autoexit = WsAutoExit} = Ws, WsLoop) ->
+ % build handshake
+ HandshakeServer = handshake(Vsn, Socket,SockMod, Headers, {Path, Q, Origin, Host, Port}),
+ % send handshake back
+ SockMod:send(Socket, HandshakeServer),
+ ?DEBUG("Sent handshake response : ~p", [HandshakeServer]),
+ Ws0 = ejabberd_ws:new(Ws#ws{origin = Origin, host = Host}, self()),
+ %?DEBUG("Ws0 : ~p",[Ws0]),
+ % add data to ws record and spawn controlling process
+ {ok, WsHandleLoopPid} = WsLoop:start_link(Ws0),
+ erlang:monitor(process, WsHandleLoopPid),
+ % set opts
+ case SockMod of
+ gen_tcp ->
+ inet:setopts(Socket, [{packet, 0}, {active, true}]);
+ _ ->
+ SockMod:setopts(Socket, [{packet, 0}, {active, true}])
+ end,
+ % start listening for incoming data
+ ws_loop(Vsn, none, Socket, WsHandleLoopPid, SockMod, WsAutoExit).
+
+
+check_websockets([], _Headers) -> false;
+check_websockets([Vsn|T], Headers) ->
+ case check_websocket(Vsn, Headers) of
+ false -> check_websockets(T, Headers);
+ Value -> Value
+ end.
+
+% Function: {true, Vsn} | false
+% Description: Check if the incoming request is a websocket request.
+check_websocket({'draft-hixie', 0} = Vsn, Headers) ->
+ %?DEBUG("testing for websocket protocol ~p", [Vsn]),
+ % set required headers
+ RequiredHeaders = [
+ {'Upgrade', "WebSocket"}, {'Connection', "Upgrade"}, {'Host', ignore}, {"Origin", ignore},
+ {"Sec-Websocket-Key1", ignore}, {"Sec-Websocket-Key2", ignore}
+ ],
+ % check for headers existance
+ case check_headers(Headers, RequiredHeaders) of
+ true ->
+ % return
+ {true, Vsn};
+ _RemainingHeaders ->
+ %?DEBUG("not protocol ~p, remaining headers: ~p", [Vsn, _RemainingHeaders]),
+ false
+ end;
+check_websocket({'draft-hixie', 68} = Vsn, Headers) ->
+ %?DEBUG("testing for websocket protocol ~p", [Vsn]),
+ % set required headers
+ RequiredHeaders = [
+ {'Upgrade', "WebSocket"}, {'Connection', "Upgrade"}, {'Host', ignore}, {"Origin", ignore}
+ ],
+ % check for headers existance
+ case check_headers(Headers, RequiredHeaders) of
+ true -> {true, Vsn};
+ _RemainingHeaders ->
+ %?DEBUG("not protocol ~p, remaining headers: ~p", [Vsn, _RemainingHeaders]),
+ false
+ end;
+check_websocket({'draft-hybi', 8} = Vsn, Headers) ->
+ %?DEBUG("testing for websocket protocol ~p", [Vsn]),
+ % set required headers
+ RequiredHeaders = [
+ {'Upgrade', "websocket"}, {'Connection', ignore}, {'Host', ignore},
+ {"Sec-Websocket-Key", ignore}, {"Sec-Websocket-Version", "8"}
+ ],
+ % check for headers existance
+ case check_headers(Headers, RequiredHeaders) of
+ true -> {true, Vsn};
+ RemainingHeaders ->
+ %%?INFO_MSG("not protocol ~p, remaining headers: ~p", [Vsn, RemainingHeaders]),
+ false
+ end;
+check_websocket({'draft-hybi', 13} = Vsn, Headers) ->
+ %?DEBUG("testing for websocket protocol ~p", [Vsn]),
+ % set required headers
+ RequiredHeaders = [
+ {'Upgrade', "websocket"}, {'Connection', ignore}, {'Host', ignore},
+ {"Sec-Websocket-Key", ignore}, {"Sec-Websocket-Version", "13"}
+ ],
+ % check for headers existance
+ case check_headers(Headers, RequiredHeaders) of
+ true -> {true, Vsn};
+ RemainingHeaders ->
+ %%?INFO_MSG("not protocol ~p, remaining headers: ~p", [Vsn, RemainingHeaders]),
+ false
+ end;
+check_websocket(_Vsn, _Headers) -> false. % not implemented
+
+% Function: true | [{RequiredTag, RequiredVal}, ..]
+% Description: Check if headers correspond to headers requirements.
+check_headers(Headers, RequiredHeaders) ->
+ F = fun({Tag, Val}) ->
+ % see if the required Tag is in the Headers
+ case lists:keyfind(Tag, 1, Headers) of
+ false -> true; % header not found, keep in list
+ {_, HVal} ->
+ %?DEBUG("check: ~p", [{Tag, HVal,Val }]),
+ case Val of
+ ignore -> false; % ignore value -> ok, remove from list
+ HVal -> false; % expected val -> ok, remove from list
+ _ -> true % val is different, keep in list
+ end
+ end
+ end,
+ case lists:filter(F, RequiredHeaders) of
+ [] -> true;
+ MissingHeaders -> MissingHeaders
+ end.
+
+% Function: List
+% Description: Builds the server handshake response.
+handshake({'draft-hixie', 0}, Sock,SocketMod, Headers, {Path, Q,Origin, Host, Port}) ->
+ % build data
+ {_, Key1} = lists:keyfind("Sec-Websocket-Key1",1, Headers),
+ {_, Key2} = lists:keyfind("Sec-Websocket-Key2",1, Headers),
+ HostPort = case lists:keyfind('Host', 1, Headers) of
+ {_, Value} -> Value;
+ _ -> string:join([Host, integer_to_list(Port)],":")
+ end,
+ % handshake needs body of the request, still need to read it [TODO: default recv timeout hard set, will be exported when WS protocol is final]
+ case SocketMod of
+ gen_tcp ->
+ inet:setopts(Sock, [{packet, raw}, {active, false}]);
+ _ ->
+ SocketMod:setopts(Sock, [{packet, raw}, {active, false}])
+ end,
+ Body = case SocketMod:recv(Sock, 8, 30*1000) of
+ {ok, Bin} -> Bin;
+ {error, timeout} ->
+ ?WARNING_MSG("timeout in reading websocket body", []),
+ <<>>;
+ _Other ->
+ ?ERROR_MSG("tcp error treating data: ~p", [_Other]),
+ <<>>
+ end,
+ QParams = lists:map(
+ fun({nokey,[]})->
+ none;
+ ({K, V})->
+ K ++ "=" ++ V
+ end, Q),
+ QString = case QParams of
+ [none]-> "";
+ QParams-> "?" ++ string:join(QParams, "&")
+ end,
+ %?DEBUG("got content in body of websocket request: ~p, ~p", [Body,string:join([Host, Path],"/")]),
+ % prepare handhsake response
+ ["HTTP/1.1 101 WebSocket Protocol Handshake\r\n",
+ "Upgrade: WebSocket\r\n",
+ "Connection: Upgrade\r\n",
+ "Sec-WebSocket-Origin: ", Origin, "\r\n",
+ "Sec-WebSocket-Location: ws://", HostPort, "/", string:join(Path,"/"),
+ QString, "\r\n\r\n",
+ build_challenge({'draft-hixie', 0}, {Key1, Key2, Body})
+ ];
+handshake({'draft-hixie', 68}, _Sock,_SocketMod, Headers, {Path, Origin, Host, Port}) ->
+ HostPort = case lists:keyfind('Host', 1, Headers) of
+ {_, Value} -> Value;
+ _ -> string:join([Host, integer_to_list(Port)],":")
+ end,
+ ["HTTP/1.1 101 Web Socket Protocol Handshake\r\n",
+ "Upgrade: WebSocket\r\n",
+ "Connection: Upgrade\r\n",
+ "WebSocket-Origin: ", Origin , "\r\n",
+ "WebSocket-Location: ws://", HostPort, "/", string:join(Path,"/"),"\r\n\r\n"
+ ];
+handshake({'draft-hybi', _}, Sock,SocketMod, Headers, {Path, Q,Origin, Host, Port}) ->
+ {_, Key} = lists:keyfind("Sec-Websocket-Key",1, Headers),
+ Hash = jlib:encode_base64(binary_to_list(sha:sha1(Key++"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))),
+ ["HTTP/1.1 101 Switching Protocols\r\n",
+ "Upgrade: websocket\r\n",
+ "Connection: Upgrade\r\n",
+ "Sec-WebSocket-Accept: ", Hash, "\r\n\r\n"
+ ].
+
+% Function: List
+% Description: Builds the challenge for a handshake response.
+% Code portions from Sergio Veiga <http://sergioveiga.com/index.php/2010/06/17/websocket-handshake-76-in-erlang/>
+build_challenge({'draft-hixie', 0}, {Key1, Key2, Key3}) ->
+ Ikey1 = [D || D <- Key1, $0 =< D, D =< $9],
+ Ikey2 = [D || D <- Key2, $0 =< D, D =< $9],
+ Blank1 = length([D || D <- Key1, D =:= 32]),
+ Blank2 = length([D || D <- Key2, D =:= 32]),
+ Part1 = list_to_integer(Ikey1) div Blank1,
+ Part2 = list_to_integer(Ikey2) div Blank2,
+ Ckey = <<Part1:4/big-unsigned-integer-unit:8, Part2:4/big-unsigned-integer-unit:8, Key3/binary>>,
+ erlang:md5(Ckey).
+
+
+ws_loop(Vsn, HandlerState, Socket, WsHandleLoopPid, SocketMode, WsAutoExit) ->
+ receive
+ {tcp, Socket, Data} ->
+ %?ERROR_MSG("[WS recv] ~p~n[Buffer state] ~p", [Data, Buffer]),
+ {NewHandlerState, ToSend} = handle_data(Vsn, HandlerState, Data, Socket, WsHandleLoopPid, SocketMode, WsAutoExit),
+ lists:foreach(fun(Pkt) ->
+ SocketMode:send(Socket, Pkt)
+ end, ToSend),
+ ws_loop(Vsn, NewHandlerState, Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
+ {tcp_closed, Socket} ->
+ ?DEBUG("tcp connection was closed, exit", []),
+ % close websocket and custom controlling loop
+ websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
+ {'DOWN', Ref, process, WsHandleLoopPid, Reason} ->
+ case Reason of
+ normal ->
+ %?DEBUG("linked websocket controlling loop stopped.", []);
+ ok;
+ _ ->
+ ?ERROR_MSG("linked websocket controlling loop crashed with reason: ~p", [Reason])
+ end,
+ % demonitor
+ erlang:demonitor(Ref),
+ % close websocket and custom controlling loop
+ websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
+ {send, Data} ->
+ %?DEBUG("sending data to websocket: ~p", [Data]),
+ SocketMode:send(Socket, encode_frame(Vsn, Data)),
+ ws_loop(Vsn, HandlerState, Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
+ shutdown ->
+ ?DEBUG("shutdown request received, closing websocket with pid ~p", [self()]),
+ % close websocket and custom controlling loop
+ websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
+ _Ignored ->
+ ?WARNING_MSG("received unexpected message, ignoring: ~p", [_Ignored]),
+ ws_loop(Vsn, HandlerState, Socket, WsHandleLoopPid, SocketMode, WsAutoExit)
+ end.
+
+encode_frame({'draft-hybi', _}, 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.
+
+encode_frame({'draft-hybi', _}=Vsn, Data) ->
+ encode_frame(Vsn, Data, 1);
+encode_frame(_, Data) ->
+ <<0, Data/binary, 255>>.
+
+process_hixie_68(none, Data) ->
+ process_hixie_68({false, <<>>}, Data);
+process_hixie_68({false, <<>>}, <<0,T/binary>>) ->
+ process_hixie_68({true, <<>>}, T);
+process_hixie_68(L, <<>>) ->
+ {L, [], []};
+process_hixie_68({_, L}, <<255,T/binary>>) ->
+ {L2, Recv, Send} = process_hixie_68({false, <<>>}, T),
+ {L2, [L|Recv], Send};
+process_hixie_68({true, L}, <<H/utf8, T/binary>>) ->
+ process_hixie_68({true, <<L/binary, H>>}, T).
+
+-record(hybi_8_state, {mask=none, offset=0, left, final_frame=true, opcode, unprocessed = <<>>, unmasked = <<>>, unmasked_msg = <<>>}).
+
+decode_hybi_8_header(<<Final:1, _:3, Opcode:4, 0:1, Len:7, Data/binary>>) when Len < 126 ->
+ {Len, Final, Opcode, none, Data};
+decode_hybi_8_header(<<Final:1, _:3, Opcode:4, 0:1, 126:7, Len:16/integer, Data/binary>>) ->
+ {Len, Final, Opcode, none, Data};
+decode_hybi_8_header(<<Final:1, _:3, Opcode:4, 0:1, 127:7, Len:64/integer, Data/binary>>) ->
+ {Len, Final, Opcode, none, Data};
+decode_hybi_8_header(<<Final:1, _:3, Opcode:4, 1:1, Len:7, Mask:4/binary, Data/binary>>) when Len < 126 ->
+ {Len, Final, Opcode, Mask, Data};
+decode_hybi_8_header(<<Final:1, _:3, Opcode:4, 1:1, 126:7, Len:16/integer, Mask:4/binary, Data/binary>>) ->
+ {Len, Final, Opcode, Mask, Data};
+decode_hybi_8_header(<<Final:1, _:3, Opcode:4, 1:1, 127:7, Len:64/integer, Mask:4/binary, Data/binary>>) ->
+ {Len, Final, Opcode, Mask, Data};
+decode_hybi_8_header(_) ->
+ none.
+
+unmask_hybi_8_int(Offset, _, <<>>, Acc) ->
+ {Acc, Offset};
+unmask_hybi_8_int(0, <<M:32>>=Mask, <<N:32, Rest/binary>>, Acc) ->
+ unmask_hybi_8_int(0, Mask, Rest, <<Acc/binary, (M bxor N):32>>);
+unmask_hybi_8_int(0, <<M:8, _/binary>>=Mask, <<N:8, Rest/binary>>, Acc) ->
+ unmask_hybi_8_int(1, Mask, Rest, <<Acc/binary, (M bxor N):8>>);
+unmask_hybi_8_int(1, <<_:8, M:8, _/binary>>=Mask, <<N:8, Rest/binary>>, Acc) ->
+ unmask_hybi_8_int(2, Mask, Rest, <<Acc/binary, (M bxor N):8>>);
+unmask_hybi_8_int(2, <<_:16, M:8, _/binary>>=Mask, <<N:8, Rest/binary>>, Acc) ->
+ unmask_hybi_8_int(3, Mask, Rest, <<Acc/binary, (M bxor N):8>>);
+unmask_hybi_8_int(3, <<_:24, M:8>>=Mask, <<N:8, Rest/binary>>, Acc) ->
+ unmask_hybi_8_int(0, Mask, Rest, <<Acc/binary, (M bxor N):8>>).
+
+unmask_hybi_8(#hybi_8_state{mask=none}=State, Data) ->
+ {State, Data};
+unmask_hybi_8(#hybi_8_state{mask=Mask, offset=Offset}=State, Data) ->
+ {Unmasked, NewOffset} = unmask_hybi_8_int(Offset, Mask, Data, <<>>),
+ {State#hybi_8_state{offset=NewOffset}, Unmasked}.
+
+
+process_hybi_8(none, Data) ->
+ process_hybi_8(#hybi_8_state{}, Data);
+process_hybi_8(State, <<>>) ->
+ {State, [], []};
+process_hybi_8(#hybi_8_state{unprocessed=none, unmasked=UnmaskedPre, left=Left}=State,
+ Data) when byte_size(Data) < Left ->
+ {State2, Unmasked} = unmask_hybi_8(State, Data),
+ {State2#hybi_8_state{left=Left-byte_size(Data), unmasked=[UnmaskedPre, Unmasked]}, [], []};
+process_hybi_8(#hybi_8_state{unprocessed=none, unmasked=UnmaskedPre, opcode=Opcode,
+ final_frame=Final, left=Left, unmasked_msg=UnmaskedMsg}=State, Data) ->
+ {_State, Unmasked} = unmask_hybi_8(State, binary_part(Data, 0, Left)),
+ Unprocessed = binary_part(Data, Left, byte_size(Data)-Left),
+ case Final of
+ true ->
+ {State3, Recv, Send} = process_hybi_8(#hybi_8_state{}, Unprocessed),
+ case Opcode of
+ 9 ->
+ Frame = encode_frame({'draft-hybi', 8}, Unprocessed, 10),
+ {State3#hybi_8_state{unmasked_msg=UnmaskedMsg}, Recv, [Frame|Send]};
+ X when X < 3 ->
+ {State3, [iolist_to_binary([UnmaskedMsg, UnmaskedPre, Unmasked])|Recv], Send};
+ _ ->
+ {State3#hybi_8_state{unmasked_msg=UnmaskedMsg}, Recv, Send}
+ end;
+ _ ->
+ process_hybi_8(#hybi_8_state{unmasked_msg=[UnmaskedMsg, UnmaskedPre, Unmasked]}, Unprocessed)
+ end;
+process_hybi_8(#hybi_8_state{unprocessed= <<>>}=State, Data) ->
+ case decode_hybi_8_header(Data) of
+ none ->
+ {State#hybi_8_state{unprocessed=Data}, [], []};
+ {Len, Final, Opcode, Mask, Rest} ->
+ process_hybi_8(State#hybi_8_state{mask=Mask, final_frame=Final==1,
+ left=Len, opcode=Opcode,
+ unprocessed=none}, Rest)
+ end;
+process_hybi_8(#hybi_8_state{unprocessed=UnprocessedPre}=State, Data) ->
+ process_hybi_8(State#hybi_8_state{unprocessed = <<>>}, <<UnprocessedPre/binary, Data/binary>>).
+
+
+% Buffering and data handling
+handle_data({'draft-hybi', _}, State, Data, _Socket, WsHandleLoopPid, _SocketMode, _WsAutoExit) ->
+ {NewState, Recv, Send} = process_hybi_8(State, Data),
+ lists:foreach(fun(El) ->
+ WsHandleLoopPid ! {browser, El}
+ end, Recv),
+ {NewState, Send};
+handle_data(_Vsn, State, Data, _Socket, WsHandleLoopPid, _SocketMode, _WsAutoExit) ->
+ {NewState, Recv, Send} = process_hixie_68(State, Data),
+ lists:foreach(fun(El) ->
+ WsHandleLoopPid ! {browser, El}
+ end, Recv),
+ {NewState, Send};
+%% Invalid input
+handle_data(_Vsn, _State, _Data, Socket, WsHandleLoopPid, SocketMode, WsAutoExit) ->
+ websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit).
+
+% Close socket and custom handling loop dependency
+websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit) ->
+ case WsAutoExit of
+ true ->
+ % kill custom handling loop process
+ exit(WsHandleLoopPid, kill);
+ false ->
+ % the killing of the custom handling loop process is handled in the loop itself -> send event
+ WsHandleLoopPid ! closed
+ end,
+ % close main socket
+ SocketMode:close(Socket).
diff --git a/src/web/ejabberd_ws.erl b/src/web/ejabberd_ws.erl
new file mode 100644
index 000000000..5ccaadf34
--- /dev/null
+++ b/src/web/ejabberd_ws.erl
@@ -0,0 +1,80 @@
+%%%----------------------------------------------------------------------
+%%% File : ejabberd_websocket.erl
+%%% Author : Eric Cestari <ecestari@process-one.net>
+%%% Purpose : Websocket support
+%%% Created : 09-10-2010 by Eric Cestari <ecestari@process-one.net>
+%%% Slightly adapted from :
+% ==========================================================================================================
+% MISULTIN - Websocket Request
+%
+% >-|-|-(°>
+%
+% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>.
+% All rights reserved.
+%
+% 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.
+% ==========================================================================================================
+-module(ejabberd_ws, [Ws, SocketPid]).
+-vsn("0.6.1").
+
+% API
+-export([raw/0, get/1, send/1]).
+
+% includes
+-include("ejabberd_http.hrl").
+
+
+% ============================ \/ API ======================================================================
+
+% Description: Returns raw websocket content.
+raw() ->
+ Ws.
+
+% Description: Get websocket info.
+get(socket) ->
+ Ws#ws.socket;
+get(socket_mode) ->
+ Ws#ws.sockmod;
+get(ip) ->
+ Ws#ws.ip;
+get(vsn) ->
+ Ws#ws.vsn;
+get(origin) ->
+ Ws#ws.origin;
+get(host) ->
+ Ws#ws.host;
+get(path) ->
+ Ws#ws.path;
+get(headers) ->
+ Ws#ws.headers.
+
+% send data
+send(Data) ->
+ SocketPid ! {send, Data}.
+
+% ============================ /\ API ======================================================================
+
+
+
+% ============================ \/ INTERNAL FUNCTIONS =======================================================
+
+% ============================ /\ INTERNAL FUNCTIONS =======================================================
diff --git a/src/web/mochijson2.erl b/src/web/mochijson2.erl
new file mode 100644
index 000000000..710ae9bce
--- /dev/null
+++ b/src/web/mochijson2.erl
@@ -0,0 +1,782 @@
+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2007 Mochi Media, Inc.
+
+%% @doc Yet another JSON (RFC 4627) library for Erlang. mochijson2 works
+%% with binaries as strings, arrays as lists (without an {array, _})
+%% wrapper and it only knows how to decode UTF-8 (and ASCII).
+
+-module(mochijson2).
+-author('bob@mochimedia.com').
+-export([encoder/1, encode/1]).
+-export([decoder/1, decode/1]).
+
+% This is a macro to placate syntax highlighters..
+-define(Q, $\").
+-define(ADV_COL(S, N), S#decoder{offset=N+S#decoder.offset,
+ column=N+S#decoder.column}).
+-define(INC_COL(S), S#decoder{offset=1+S#decoder.offset,
+ column=1+S#decoder.column}).
+-define(INC_LINE(S), S#decoder{offset=1+S#decoder.offset,
+ column=1,
+ line=1+S#decoder.line}).
+-define(INC_CHAR(S, C),
+ case C of
+ $\n ->
+ S#decoder{column=1,
+ line=1+S#decoder.line,
+ offset=1+S#decoder.offset};
+ _ ->
+ S#decoder{column=1+S#decoder.column,
+ offset=1+S#decoder.offset}
+ end).
+-define(IS_WHITESPACE(C),
+ (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)).
+
+%% @type iolist() = [char() | binary() | iolist()]
+%% @type iodata() = iolist() | binary()
+%% @type json_string() = atom | binary()
+%% @type json_number() = integer() | float()
+%% @type json_array() = [json_term()]
+%% @type json_object() = {struct, [{json_string(), json_term()}]}
+%% @type json_iolist() = {json, iolist()}
+%% @type json_term() = json_string() | json_number() | json_array() |
+%% json_object() | json_iolist()
+
+-record(encoder, {handler=null,
+ utf8=false}).
+
+-record(decoder, {object_hook=null,
+ offset=0,
+ line=1,
+ column=1,
+ state=null}).
+
+%% @spec encoder([encoder_option()]) -> function()
+%% @doc Create an encoder/1 with the given options.
+%% @type encoder_option() = handler_option() | utf8_option()
+%% @type utf8_option() = boolean(). Emit unicode as utf8 (default - false)
+encoder(Options) ->
+ State = parse_encoder_options(Options, #encoder{}),
+ fun (O) -> json_encode(O, State) end.
+
+%% @spec encode(json_term()) -> iolist()
+%% @doc Encode the given as JSON to an iolist.
+encode(Any) ->
+ json_encode(Any, #encoder{}).
+
+%% @spec decoder([decoder_option()]) -> function()
+%% @doc Create a decoder/1 with the given options.
+decoder(Options) ->
+ State = parse_decoder_options(Options, #decoder{}),
+ fun (O) -> json_decode(O, State) end.
+
+%% @spec decode(iolist()) -> json_term()
+%% @doc Decode the given iolist to Erlang terms.
+decode(S) ->
+ json_decode(S, #decoder{}).
+
+%% Internal API
+
+parse_encoder_options([], State) ->
+ State;
+parse_encoder_options([{handler, Handler} | Rest], State) ->
+ parse_encoder_options(Rest, State#encoder{handler=Handler});
+parse_encoder_options([{utf8, Switch} | Rest], State) ->
+ parse_encoder_options(Rest, State#encoder{utf8=Switch}).
+
+parse_decoder_options([], State) ->
+ State;
+parse_decoder_options([{object_hook, Hook} | Rest], State) ->
+ parse_decoder_options(Rest, State#decoder{object_hook=Hook}).
+
+json_encode(true, _State) ->
+ <<"true">>;
+json_encode(false, _State) ->
+ <<"false">>;
+json_encode(null, _State) ->
+ <<"null">>;
+json_encode(I, _State) when is_integer(I) andalso I >= -2147483648 andalso I =< 2147483647 ->
+ %% Anything outside of 32-bit integers should be encoded as a float
+ integer_to_list(I);
+json_encode(I, _State) when is_integer(I) ->
+ mochinum:digits(float(I));
+json_encode(F, _State) when is_float(F) ->
+ mochinum:digits(F);
+json_encode(S, State) when is_binary(S); is_atom(S) ->
+ json_encode_string(S, State);
+json_encode(Array, State) when is_list(Array) ->
+ json_encode_array(Array, State);
+json_encode({struct, Props}, State) when is_list(Props) ->
+ json_encode_proplist(Props, State);
+json_encode({json, IoList}, _State) ->
+ IoList;
+json_encode(Bad, #encoder{handler=null}) ->
+ exit({json_encode, {bad_term, Bad}});
+json_encode(Bad, State=#encoder{handler=Handler}) ->
+ json_encode(Handler(Bad), State).
+
+json_encode_array([], _State) ->
+ <<"[]">>;
+json_encode_array(L, State) ->
+ F = fun (O, Acc) ->
+ [$,, json_encode(O, State) | Acc]
+ end,
+ [$, | Acc1] = lists:foldl(F, "[", L),
+ lists:reverse([$\] | Acc1]).
+
+json_encode_proplist([], _State) ->
+ <<"{}">>;
+json_encode_proplist(Props, State) ->
+ F = fun ({K, V}, Acc) ->
+ KS = json_encode_string(K, State),
+ VS = json_encode(V, State),
+ [$,, VS, $:, KS | Acc]
+ end,
+ [$, | Acc1] = lists:foldl(F, "{", Props),
+ lists:reverse([$\} | Acc1]).
+
+json_encode_string(A, State) when is_atom(A) ->
+ L = atom_to_list(A),
+ case json_string_is_safe(L) of
+ true ->
+ [?Q, L, ?Q];
+ false ->
+ json_encode_string_unicode(xmerl_ucs:from_utf8(L), State, [?Q])
+ end;
+json_encode_string(B, State) when is_binary(B) ->
+ case json_bin_is_safe(B) of
+ true ->
+ [?Q, B, ?Q];
+ false ->
+ json_encode_string_unicode(xmerl_ucs:from_utf8(B), State, [?Q])
+ end;
+json_encode_string(I, _State) when is_integer(I) ->
+ [?Q, integer_to_list(I), ?Q];
+json_encode_string(L, State) when is_list(L) ->
+ case json_string_is_safe(L) of
+ true ->
+ [?Q, L, ?Q];
+ false ->
+ json_encode_string_unicode(L, State, [?Q])
+ end.
+
+json_string_is_safe([]) ->
+ true;
+json_string_is_safe([C | Rest]) ->
+ case C of
+ ?Q ->
+ false;
+ $\\ ->
+ false;
+ $\b ->
+ false;
+ $\f ->
+ false;
+ $\n ->
+ false;
+ $\r ->
+ false;
+ $\t ->
+ false;
+ C when C >= 0, C < $\s; C >= 16#7f, C =< 16#10FFFF ->
+ false;
+ C when C < 16#7f ->
+ json_string_is_safe(Rest);
+ _ ->
+ false
+ end.
+
+json_bin_is_safe(<<>>) ->
+ true;
+json_bin_is_safe(<<C, Rest/binary>>) ->
+ case C of
+ ?Q ->
+ false;
+ $\\ ->
+ false;
+ $\b ->
+ false;
+ $\f ->
+ false;
+ $\n ->
+ false;
+ $\r ->
+ false;
+ $\t ->
+ false;
+ C when C >= 0, C < $\s; C >= 16#7f ->
+ false;
+ C when C < 16#7f ->
+ json_bin_is_safe(Rest)
+ end.
+
+json_encode_string_unicode([], _State, Acc) ->
+ lists:reverse([$\" | Acc]);
+json_encode_string_unicode([C | Cs], State, Acc) ->
+ Acc1 = case C of
+ ?Q ->
+ [?Q, $\\ | Acc];
+ %% Escaping solidus is only useful when trying to protect
+ %% against "</script>" injection attacks which are only
+ %% possible when JSON is inserted into a HTML document
+ %% in-line. mochijson2 does not protect you from this, so
+ %% if you do insert directly into HTML then you need to
+ %% uncomment the following case or escape the output of encode.
+ %%
+ %% $/ ->
+ %% [$/, $\\ | Acc];
+ %%
+ $\\ ->
+ [$\\, $\\ | Acc];
+ $\b ->
+ [$b, $\\ | Acc];
+ $\f ->
+ [$f, $\\ | Acc];
+ $\n ->
+ [$n, $\\ | Acc];
+ $\r ->
+ [$r, $\\ | Acc];
+ $\t ->
+ [$t, $\\ | Acc];
+ C when C >= 0, C < $\s ->
+ [unihex(C) | Acc];
+ C when C >= 16#7f, C =< 16#10FFFF, State#encoder.utf8 ->
+ [xmerl_ucs:to_utf8(C) | Acc];
+ C when C >= 16#7f, C =< 16#10FFFF, not State#encoder.utf8 ->
+ [unihex(C) | Acc];
+ C when C < 16#7f ->
+ [C | Acc];
+ _ ->
+ exit({json_encode, {bad_char, C}})
+ end,
+ json_encode_string_unicode(Cs, State, Acc1).
+
+hexdigit(C) when C >= 0, C =< 9 ->
+ C + $0;
+hexdigit(C) when C =< 15 ->
+ C + $a - 10.
+
+unihex(C) when C < 16#10000 ->
+ <<D3:4, D2:4, D1:4, D0:4>> = <<C:16>>,
+ Digits = [hexdigit(D) || D <- [D3, D2, D1, D0]],
+ [$\\, $u | Digits];
+unihex(C) when C =< 16#10FFFF ->
+ N = C - 16#10000,
+ S1 = 16#d800 bor ((N bsr 10) band 16#3ff),
+ S2 = 16#dc00 bor (N band 16#3ff),
+ [unihex(S1), unihex(S2)].
+
+json_decode(L, S) when is_list(L) ->
+ json_decode(iolist_to_binary(L), S);
+json_decode(B, S) ->
+ {Res, S1} = decode1(B, S),
+ {eof, _} = tokenize(B, S1#decoder{state=trim}),
+ Res.
+
+decode1(B, S=#decoder{state=null}) ->
+ case tokenize(B, S#decoder{state=any}) of
+ {{const, C}, S1} ->
+ {C, S1};
+ {start_array, S1} ->
+ decode_array(B, S1);
+ {start_object, S1} ->
+ decode_object(B, S1)
+ end.
+
+make_object(V, #decoder{object_hook=null}) ->
+ V;
+make_object(V, #decoder{object_hook=Hook}) ->
+ Hook(V).
+
+decode_object(B, S) ->
+ decode_object(B, S#decoder{state=key}, []).
+
+decode_object(B, S=#decoder{state=key}, Acc) ->
+ case tokenize(B, S) of
+ {end_object, S1} ->
+ V = make_object({struct, lists:reverse(Acc)}, S1),
+ {V, S1#decoder{state=null}};
+ {{const, K}, S1} ->
+ {colon, S2} = tokenize(B, S1),
+ {V, S3} = decode1(B, S2#decoder{state=null}),
+ decode_object(B, S3#decoder{state=comma}, [{K, V} | Acc])
+ end;
+decode_object(B, S=#decoder{state=comma}, Acc) ->
+ case tokenize(B, S) of
+ {end_object, S1} ->
+ V = make_object({struct, lists:reverse(Acc)}, S1),
+ {V, S1#decoder{state=null}};
+ {comma, S1} ->
+ decode_object(B, S1#decoder{state=key}, Acc)
+ end.
+
+decode_array(B, S) ->
+ decode_array(B, S#decoder{state=any}, []).
+
+decode_array(B, S=#decoder{state=any}, Acc) ->
+ case tokenize(B, S) of
+ {end_array, S1} ->
+ {lists:reverse(Acc), S1#decoder{state=null}};
+ {start_array, S1} ->
+ {Array, S2} = decode_array(B, S1),
+ decode_array(B, S2#decoder{state=comma}, [Array | Acc]);
+ {start_object, S1} ->
+ {Array, S2} = decode_object(B, S1),
+ decode_array(B, S2#decoder{state=comma}, [Array | Acc]);
+ {{const, Const}, S1} ->
+ decode_array(B, S1#decoder{state=comma}, [Const | Acc])
+ end;
+decode_array(B, S=#decoder{state=comma}, Acc) ->
+ case tokenize(B, S) of
+ {end_array, S1} ->
+ {lists:reverse(Acc), S1#decoder{state=null}};
+ {comma, S1} ->
+ decode_array(B, S1#decoder{state=any}, Acc)
+ end.
+
+tokenize_string(B, S=#decoder{offset=O}) ->
+ case tokenize_string_fast(B, O) of
+ {escape, O1} ->
+ Length = O1 - O,
+ S1 = ?ADV_COL(S, Length),
+ <<_:O/binary, Head:Length/binary, _/binary>> = B,
+ tokenize_string(B, S1, lists:reverse(binary_to_list(Head)));
+ O1 ->
+ Length = O1 - O,
+ <<_:O/binary, String:Length/binary, ?Q, _/binary>> = B,
+ {{const, String}, ?ADV_COL(S, Length + 1)}
+ end.
+
+tokenize_string_fast(B, O) ->
+ case B of
+ <<_:O/binary, ?Q, _/binary>> ->
+ O;
+ <<_:O/binary, $\\, _/binary>> ->
+ {escape, O};
+ <<_:O/binary, C1, _/binary>> when C1 < 128 ->
+ tokenize_string_fast(B, 1 + O);
+ <<_:O/binary, C1, C2, _/binary>> when C1 >= 194, C1 =< 223,
+ C2 >= 128, C2 =< 191 ->
+ tokenize_string_fast(B, 2 + O);
+ <<_:O/binary, C1, C2, C3, _/binary>> when C1 >= 224, C1 =< 239,
+ C2 >= 128, C2 =< 191,
+ C3 >= 128, C3 =< 191 ->
+ tokenize_string_fast(B, 3 + O);
+ <<_:O/binary, C1, C2, C3, C4, _/binary>> when C1 >= 240, C1 =< 244,
+ C2 >= 128, C2 =< 191,
+ C3 >= 128, C3 =< 191,
+ C4 >= 128, C4 =< 191 ->
+ tokenize_string_fast(B, 4 + O);
+ _ ->
+ throw(invalid_utf8)
+ end.
+
+tokenize_string(B, S=#decoder{offset=O}, Acc) ->
+ case B of
+ <<_:O/binary, ?Q, _/binary>> ->
+ {{const, iolist_to_binary(lists:reverse(Acc))}, ?INC_COL(S)};
+ <<_:O/binary, "\\\"", _/binary>> ->
+ tokenize_string(B, ?ADV_COL(S, 2), [$\" | Acc]);
+ <<_:O/binary, "\\\\", _/binary>> ->
+ tokenize_string(B, ?ADV_COL(S, 2), [$\\ | Acc]);
+ <<_:O/binary, "\\/", _/binary>> ->
+ tokenize_string(B, ?ADV_COL(S, 2), [$/ | Acc]);
+ <<_:O/binary, "\\b", _/binary>> ->
+ tokenize_string(B, ?ADV_COL(S, 2), [$\b | Acc]);
+ <<_:O/binary, "\\f", _/binary>> ->
+ tokenize_string(B, ?ADV_COL(S, 2), [$\f | Acc]);
+ <<_:O/binary, "\\n", _/binary>> ->
+ tokenize_string(B, ?ADV_COL(S, 2), [$\n | Acc]);
+ <<_:O/binary, "\\r", _/binary>> ->
+ tokenize_string(B, ?ADV_COL(S, 2), [$\r | Acc]);
+ <<_:O/binary, "\\t", _/binary>> ->
+ tokenize_string(B, ?ADV_COL(S, 2), [$\t | Acc]);
+ <<_:O/binary, "\\u", C3, C2, C1, C0, Rest/binary>> ->
+ C = erlang:list_to_integer([C3, C2, C1, C0], 16),
+ if C > 16#D7FF, C < 16#DC00 ->
+ %% coalesce UTF-16 surrogate pair
+ <<"\\u", D3, D2, D1, D0, _/binary>> = Rest,
+ D = erlang:list_to_integer([D3,D2,D1,D0], 16),
+ [CodePoint] = xmerl_ucs:from_utf16be(<<C:16/big-unsigned-integer,
+ D:16/big-unsigned-integer>>),
+ Acc1 = lists:reverse(xmerl_ucs:to_utf8(CodePoint), Acc),
+ tokenize_string(B, ?ADV_COL(S, 12), Acc1);
+ true ->
+ Acc1 = lists:reverse(xmerl_ucs:to_utf8(C), Acc),
+ tokenize_string(B, ?ADV_COL(S, 6), Acc1)
+ end;
+ <<_:O/binary, C, _/binary>> ->
+ tokenize_string(B, ?INC_CHAR(S, C), [C | Acc])
+ end.
+
+tokenize_number(B, S) ->
+ case tokenize_number(B, sign, S, []) of
+ {{int, Int}, S1} ->
+ {{const, list_to_integer(Int)}, S1};
+ {{float, Float}, S1} ->
+ {{const, list_to_float(Float)}, S1}
+ end.
+
+tokenize_number(B, sign, S=#decoder{offset=O}, []) ->
+ case B of
+ <<_:O/binary, $-, _/binary>> ->
+ tokenize_number(B, int, ?INC_COL(S), [$-]);
+ _ ->
+ tokenize_number(B, int, S, [])
+ end;
+tokenize_number(B, int, S=#decoder{offset=O}, Acc) ->
+ case B of
+ <<_:O/binary, $0, _/binary>> ->
+ tokenize_number(B, frac, ?INC_COL(S), [$0 | Acc]);
+ <<_:O/binary, C, _/binary>> when C >= $1 andalso C =< $9 ->
+ tokenize_number(B, int1, ?INC_COL(S), [C | Acc])
+ end;
+tokenize_number(B, int1, S=#decoder{offset=O}, Acc) ->
+ case B of
+ <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 ->
+ tokenize_number(B, int1, ?INC_COL(S), [C | Acc]);
+ _ ->
+ tokenize_number(B, frac, S, Acc)
+ end;
+tokenize_number(B, frac, S=#decoder{offset=O}, Acc) ->
+ case B of
+ <<_:O/binary, $., C, _/binary>> when C >= $0, C =< $9 ->
+ tokenize_number(B, frac1, ?ADV_COL(S, 2), [C, $. | Acc]);
+ <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E ->
+ tokenize_number(B, esign, ?INC_COL(S), [$e, $0, $. | Acc]);
+ _ ->
+ {{int, lists:reverse(Acc)}, S}
+ end;
+tokenize_number(B, frac1, S=#decoder{offset=O}, Acc) ->
+ case B of
+ <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 ->
+ tokenize_number(B, frac1, ?INC_COL(S), [C | Acc]);
+ <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E ->
+ tokenize_number(B, esign, ?INC_COL(S), [$e | Acc]);
+ _ ->
+ {{float, lists:reverse(Acc)}, S}
+ end;
+tokenize_number(B, esign, S=#decoder{offset=O}, Acc) ->
+ case B of
+ <<_:O/binary, C, _/binary>> when C =:= $- orelse C=:= $+ ->
+ tokenize_number(B, eint, ?INC_COL(S), [C | Acc]);
+ _ ->
+ tokenize_number(B, eint, S, Acc)
+ end;
+tokenize_number(B, eint, S=#decoder{offset=O}, Acc) ->
+ case B of
+ <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 ->
+ tokenize_number(B, eint1, ?INC_COL(S), [C | Acc])
+ end;
+tokenize_number(B, eint1, S=#decoder{offset=O}, Acc) ->
+ case B of
+ <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 ->
+ tokenize_number(B, eint1, ?INC_COL(S), [C | Acc]);
+ _ ->
+ {{float, lists:reverse(Acc)}, S}
+ end.
+
+tokenize(B, S=#decoder{offset=O}) ->
+ case B of
+ <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) ->
+ tokenize(B, ?INC_CHAR(S, C));
+ <<_:O/binary, "{", _/binary>> ->
+ {start_object, ?INC_COL(S)};
+ <<_:O/binary, "}", _/binary>> ->
+ {end_object, ?INC_COL(S)};
+ <<_:O/binary, "[", _/binary>> ->
+ {start_array, ?INC_COL(S)};
+ <<_:O/binary, "]", _/binary>> ->
+ {end_array, ?INC_COL(S)};
+ <<_:O/binary, ",", _/binary>> ->
+ {comma, ?INC_COL(S)};
+ <<_:O/binary, ":", _/binary>> ->
+ {colon, ?INC_COL(S)};
+ <<_:O/binary, "null", _/binary>> ->
+ {{const, null}, ?ADV_COL(S, 4)};
+ <<_:O/binary, "true", _/binary>> ->
+ {{const, true}, ?ADV_COL(S, 4)};
+ <<_:O/binary, "false", _/binary>> ->
+ {{const, false}, ?ADV_COL(S, 5)};
+ <<_:O/binary, "\"", _/binary>> ->
+ tokenize_string(B, ?INC_COL(S));
+ <<_:O/binary, C, _/binary>> when (C >= $0 andalso C =< $9)
+ orelse C =:= $- ->
+ tokenize_number(B, S);
+ <<_:O/binary>> ->
+ trim = S#decoder.state,
+ {eof, S}
+ end.
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+
+%% testing constructs borrowed from the Yaws JSON implementation.
+
+%% Create an object from a list of Key/Value pairs.
+
+obj_new() ->
+ {struct, []}.
+
+is_obj({struct, Props}) ->
+ F = fun ({K, _}) when is_binary(K) -> true end,
+ lists:all(F, Props).
+
+obj_from_list(Props) ->
+ Obj = {struct, Props},
+ ?assert(is_obj(Obj)),
+ Obj.
+
+%% Test for equivalence of Erlang terms.
+%% Due to arbitrary order of construction, equivalent objects might
+%% compare unequal as erlang terms, so we need to carefully recurse
+%% through aggregates (tuples and objects).
+
+equiv({struct, Props1}, {struct, Props2}) ->
+ equiv_object(Props1, Props2);
+equiv(L1, L2) when is_list(L1), is_list(L2) ->
+ equiv_list(L1, L2);
+equiv(N1, N2) when is_number(N1), is_number(N2) -> N1 == N2;
+equiv(B1, B2) when is_binary(B1), is_binary(B2) -> B1 == B2;
+equiv(A, A) when A =:= true orelse A =:= false orelse A =:= null -> true.
+
+%% Object representation and traversal order is unknown.
+%% Use the sledgehammer and sort property lists.
+
+equiv_object(Props1, Props2) ->
+ L1 = lists:keysort(1, Props1),
+ L2 = lists:keysort(1, Props2),
+ Pairs = lists:zip(L1, L2),
+ true = lists:all(fun({{K1, V1}, {K2, V2}}) ->
+ equiv(K1, K2) and equiv(V1, V2)
+ end, Pairs).
+
+%% Recursively compare tuple elements for equivalence.
+
+equiv_list([], []) ->
+ true;
+equiv_list([V1 | L1], [V2 | L2]) ->
+ equiv(V1, V2) andalso equiv_list(L1, L2).
+
+decode_test() ->
+ [1199344435545.0, 1] = decode(<<"[1199344435545.0,1]">>),
+ <<16#F0,16#9D,16#9C,16#95>> = decode([34,"\\ud835","\\udf15",34]).
+
+e2j_vec_test() ->
+ test_one(e2j_test_vec(utf8), 1).
+
+test_one([], _N) ->
+ %% io:format("~p tests passed~n", [N-1]),
+ ok;
+test_one([{E, J} | Rest], N) ->
+ %% io:format("[~p] ~p ~p~n", [N, E, J]),
+ true = equiv(E, decode(J)),
+ true = equiv(E, decode(encode(E))),
+ test_one(Rest, 1+N).
+
+e2j_test_vec(utf8) ->
+ [
+ {1, "1"},
+ {3.1416, "3.14160"}, %% text representation may truncate, trail zeroes
+ {-1, "-1"},
+ {-3.1416, "-3.14160"},
+ {12.0e10, "1.20000e+11"},
+ {1.234E+10, "1.23400e+10"},
+ {-1.234E-10, "-1.23400e-10"},
+ {10.0, "1.0e+01"},
+ {123.456, "1.23456E+2"},
+ {10.0, "1e1"},
+ {<<"foo">>, "\"foo\""},
+ {<<"foo", 5, "bar">>, "\"foo\\u0005bar\""},
+ {<<"">>, "\"\""},
+ {<<"\n\n\n">>, "\"\\n\\n\\n\""},
+ {<<"\" \b\f\r\n\t\"">>, "\"\\\" \\b\\f\\r\\n\\t\\\"\""},
+ {obj_new(), "{}"},
+ {obj_from_list([{<<"foo">>, <<"bar">>}]), "{\"foo\":\"bar\"}"},
+ {obj_from_list([{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]),
+ "{\"foo\":\"bar\",\"baz\":123}"},
+ {[], "[]"},
+ {[[]], "[[]]"},
+ {[1, <<"foo">>], "[1,\"foo\"]"},
+
+ %% json array in a json object
+ {obj_from_list([{<<"foo">>, [123]}]),
+ "{\"foo\":[123]}"},
+
+ %% json object in a json object
+ {obj_from_list([{<<"foo">>, obj_from_list([{<<"bar">>, true}])}]),
+ "{\"foo\":{\"bar\":true}}"},
+
+ %% fold evaluation order
+ {obj_from_list([{<<"foo">>, []},
+ {<<"bar">>, obj_from_list([{<<"baz">>, true}])},
+ {<<"alice">>, <<"bob">>}]),
+ "{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}"},
+
+ %% json object in a json array
+ {[-123, <<"foo">>, obj_from_list([{<<"bar">>, []}]), null],
+ "[-123,\"foo\",{\"bar\":[]},null]"}
+ ].
+
+%% test utf8 encoding
+encoder_utf8_test() ->
+ %% safe conversion case (default)
+ [34,"\\u0001","\\u0442","\\u0435","\\u0441","\\u0442",34] =
+ encode(<<1,"\321\202\320\265\321\201\321\202">>),
+
+ %% raw utf8 output (optional)
+ Enc = mochijson2:encoder([{utf8, true}]),
+ [34,"\\u0001",[209,130],[208,181],[209,129],[209,130],34] =
+ Enc(<<1,"\321\202\320\265\321\201\321\202">>).
+
+input_validation_test() ->
+ Good = [
+ {16#00A3, <<?Q, 16#C2, 16#A3, ?Q>>}, %% pound
+ {16#20AC, <<?Q, 16#E2, 16#82, 16#AC, ?Q>>}, %% euro
+ {16#10196, <<?Q, 16#F0, 16#90, 16#86, 16#96, ?Q>>} %% denarius
+ ],
+ lists:foreach(fun({CodePoint, UTF8}) ->
+ Expect = list_to_binary(xmerl_ucs:to_utf8(CodePoint)),
+ Expect = decode(UTF8)
+ end, Good),
+
+ Bad = [
+ %% 2nd, 3rd, or 4th byte of a multi-byte sequence w/o leading byte
+ <<?Q, 16#80, ?Q>>,
+ %% missing continuations, last byte in each should be 80-BF
+ <<?Q, 16#C2, 16#7F, ?Q>>,
+ <<?Q, 16#E0, 16#80,16#7F, ?Q>>,
+ <<?Q, 16#F0, 16#80, 16#80, 16#7F, ?Q>>,
+ %% we don't support code points > 10FFFF per RFC 3629
+ <<?Q, 16#F5, 16#80, 16#80, 16#80, ?Q>>
+ ],
+ lists:foreach(
+ fun(X) ->
+ ok = try decode(X) catch invalid_utf8 -> ok end,
+ %% could be {ucs,{bad_utf8_character_code}} or
+ %% {json_encode,{bad_char,_}}
+ {'EXIT', _} = (catch encode(X))
+ end, Bad).
+
+inline_json_test() ->
+ ?assertEqual(<<"\"iodata iodata\"">>,
+ iolist_to_binary(
+ encode({json, [<<"\"iodata">>, " iodata\""]}))),
+ ?assertEqual({struct, [{<<"key">>, <<"iodata iodata">>}]},
+ decode(
+ encode({struct,
+ [{key, {json, [<<"\"iodata">>, " iodata\""]}}]}))),
+ ok.
+
+big_unicode_test() ->
+ UTF8Seq = list_to_binary(xmerl_ucs:to_utf8(16#0001d120)),
+ ?assertEqual(
+ <<"\"\\ud834\\udd20\"">>,
+ iolist_to_binary(encode(UTF8Seq))),
+ ?assertEqual(
+ UTF8Seq,
+ decode(iolist_to_binary(encode(UTF8Seq)))),
+ ok.
+
+custom_decoder_test() ->
+ ?assertEqual(
+ {struct, [{<<"key">>, <<"value">>}]},
+ (decoder([]))("{\"key\": \"value\"}")),
+ F = fun ({struct, [{<<"key">>, <<"value">>}]}) -> win end,
+ ?assertEqual(
+ win,
+ (decoder([{object_hook, F}]))("{\"key\": \"value\"}")),
+ ok.
+
+atom_test() ->
+ %% JSON native atoms
+ [begin
+ ?assertEqual(A, decode(atom_to_list(A))),
+ ?assertEqual(iolist_to_binary(atom_to_list(A)),
+ iolist_to_binary(encode(A)))
+ end || A <- [true, false, null]],
+ %% Atom to string
+ ?assertEqual(
+ <<"\"foo\"">>,
+ iolist_to_binary(encode(foo))),
+ ?assertEqual(
+ <<"\"\\ud834\\udd20\"">>,
+ iolist_to_binary(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))),
+ ok.
+
+key_encode_test() ->
+ %% Some forms are accepted as keys that would not be strings in other
+ %% cases
+ ?assertEqual(
+ <<"{\"foo\":1}">>,
+ iolist_to_binary(encode({struct, [{foo, 1}]}))),
+ ?assertEqual(
+ <<"{\"foo\":1}">>,
+ iolist_to_binary(encode({struct, [{<<"foo">>, 1}]}))),
+ ?assertEqual(
+ <<"{\"foo\":1}">>,
+ iolist_to_binary(encode({struct, [{"foo", 1}]}))),
+ ?assertEqual(
+ <<"{\"\\ud834\\udd20\":1}">>,
+ iolist_to_binary(
+ encode({struct, [{[16#0001d120], 1}]}))),
+ ?assertEqual(
+ <<"{\"1\":1}">>,
+ iolist_to_binary(encode({struct, [{1, 1}]}))),
+ ok.
+
+unsafe_chars_test() ->
+ Chars = "\"\\\b\f\n\r\t",
+ [begin
+ ?assertEqual(false, json_string_is_safe([C])),
+ ?assertEqual(false, json_bin_is_safe(<<C>>)),
+ ?assertEqual(<<C>>, decode(encode(<<C>>)))
+ end || C <- Chars],
+ ?assertEqual(
+ false,
+ json_string_is_safe([16#0001d120])),
+ ?assertEqual(
+ false,
+ json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8(16#0001d120)))),
+ ?assertEqual(
+ [16#0001d120],
+ xmerl_ucs:from_utf8(
+ binary_to_list(
+ decode(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))))),
+ ?assertEqual(
+ false,
+ json_string_is_safe([16#110000])),
+ ?assertEqual(
+ false,
+ json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8([16#110000])))),
+ %% solidus can be escaped but isn't unsafe by default
+ ?assertEqual(
+ <<"/">>,
+ decode(<<"\"\\/\"">>)),
+ ok.
+
+int_test() ->
+ ?assertEqual(0, decode("0")),
+ ?assertEqual(1, decode("1")),
+ ?assertEqual(11, decode("11")),
+ ok.
+
+float_fallback_test() ->
+ ?assertEqual(<<"-2147483649.0">>, iolist_to_binary(encode(-2147483649))),
+ ?assertEqual(<<"2147483648.0">>, iolist_to_binary(encode(2147483648))),
+ ok.
+
+handler_test() ->
+ ?assertEqual(
+ {'EXIT',{json_encode,{bad_term,{}}}},
+ catch encode({})),
+ F = fun ({}) -> [] end,
+ ?assertEqual(
+ <<"[]">>,
+ iolist_to_binary((encoder([{handler, F}]))({}))),
+ ok.
+
+-endif. \ No newline at end of file
diff --git a/src/web/mod_bosh.erl b/src/web/mod_bosh.erl
new file mode 100644
index 000000000..64b85357b
--- /dev/null
+++ b/src/web/mod_bosh.erl
@@ -0,0 +1,212 @@
+%%%-------------------------------------------------------------------
+%%% File : mod_bosh.erl
+%%% Author : Evgeniy Khramtsov <ekhramtsov@process-one.net>
+%%% Purpose : This module acts as a bridge to ejabberd_bosh which implements
+%%% the real stuff, this is to handle the new pluggable architecture
+%%% for extending ejabberd's http service.
+%%% Created : 20 Jul 2011 by Evgeniy Khramtsov <ekhramtsov@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2011 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+-module(mod_bosh).
+-author('steve@zeank.in-berlin.de').
+
+%%-define(ejabberd_debug, true).
+
+-behaviour(gen_mod).
+
+-export([
+ start/2,
+ stop/1,
+ process/2,
+ open_session/2,
+ close_session/1,
+ find_session/1,
+ node_up/1,
+ node_down/1,
+ migrate/3
+ ]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("ejabberd_http.hrl").
+-include("bosh.hrl").
+
+-record(bosh, {sid, pid}).
+
+%%%----------------------------------------------------------------------
+%%% API
+%%%----------------------------------------------------------------------
+
+process([], #request{method = 'POST',
+ data = []}) ->
+ ?DEBUG("Bad Request: no data", []),
+ {400, ?HEADER, {xmlelement, "h1", [],
+ [{xmlcdata, "400 Bad Request"}]}};
+process([], #request{method = 'POST',
+ data = Data,
+ ip = IP}) ->
+ ?DEBUG("Incoming data: ~s", [Data]),
+ ejabberd_bosh:process_request(Data, IP);
+process([], #request{method = 'GET',
+ data = []}) ->
+ {200, ?HEADER, get_human_html_xmlel()};
+process([], #request{method = 'OPTIONS',
+ data = []}) ->
+ {200, ?OPTIONS_HEADER, []};
+process(_Path, _Request) ->
+ ?DEBUG("Bad Request: ~p", [_Request]),
+ {400, ?HEADER, {xmlelement, "h1", [],
+ [{xmlcdata, "400 Bad Request"}]}}.
+
+get_human_html_xmlel() ->
+ Heading = "ejabberd " ++ atom_to_list(?MODULE),
+ {xmlelement, "html", [{"xmlns", "http://www.w3.org/1999/xhtml"}],
+ [{xmlelement, "head", [],
+ [{xmlelement, "title", [], [{xmlcdata, Heading}]}]},
+ {xmlelement, "body", [],
+ [{xmlelement, "h1", [], [{xmlcdata, Heading}]},
+ {xmlelement, "p", [],
+ [{xmlcdata, "An implementation of "},
+ {xmlelement, "a",
+ [{"href", "http://xmpp.org/extensions/xep-0206.html"}],
+ [{xmlcdata, "XMPP over BOSH (XEP-0206)"}]}]},
+ {xmlelement, "p", [],
+ [{xmlcdata, "This web page is only informative. "
+ "To use HTTP-Bind you need a Jabber/XMPP client that supports it."}
+ ]}
+ ]}]}.
+
+open_session(SID, Pid) ->
+ mnesia:dirty_write(#bosh{sid = SID, pid = Pid}).
+
+close_session(SID) ->
+ mnesia:dirty_delete(bosh, SID).
+
+find_session(SID) ->
+ Node = ejabberd_cluster:get_node(SID),
+ case rpc:call(Node, mnesia, dirty_read, [bosh, SID], 5000) of
+ [#bosh{pid = Pid}] ->
+ {ok, Pid};
+ _ ->
+ error
+ end.
+
+migrate(_Node, _UpOrDown, After) ->
+ Rs = mnesia:dirty_select(
+ bosh,
+ [{#bosh{sid = '$1', pid = '$2', _ = '_'},
+ [],
+ ['$$']}]),
+ lists:foreach(
+ fun([SID, Pid]) ->
+ case ejabberd_cluster:get_node(SID) of
+ Node when Node /= node() ->
+ ejabberd_bosh:migrate(Pid, Node, random:uniform(After));
+ _ ->
+ ok
+ end
+ end, Rs).
+
+node_up(_Node) ->
+ copy_entries(mnesia:dirty_first(bosh)).
+
+node_down(Node) when Node == node() ->
+ copy_entries(mnesia:dirty_first(bosh));
+node_down(_) ->
+ ok.
+
+copy_entries('$end_of_table') ->
+ ok;
+copy_entries(Key) ->
+ case mnesia:dirty_read(bosh, Key) of
+ [#bosh{sid = SID} = Entry] ->
+ case ejabberd_cluster:get_node_new(SID) of
+ Node when node() /= Node ->
+ rpc:cast(Node, mnesia, dirty_write, [Entry]);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end,
+ copy_entries(mnesia:dirty_next(bosh, Key)).
+
+%%%----------------------------------------------------------------------
+%%% BEHAVIOUR CALLBACKS
+%%%----------------------------------------------------------------------
+start(Host, _Opts) ->
+ start_hook_handler(),
+ setup_database(),
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ ChildSpec =
+ {Proc,
+ {ejabberd_tmp_sup, start_link,
+ [Proc, ejabberd_bosh]},
+ permanent,
+ infinity,
+ supervisor,
+ [ejabberd_tmp_sup]},
+ supervisor:start_child(ejabberd_sup, ChildSpec).
+
+stop(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ supervisor:terminate_child(ejabberd_sup, Proc),
+ supervisor:delete_child(ejabberd_sup, Proc).
+
+setup_database() ->
+ mnesia:create_table(bosh,
+ [{ram_copies, [node()]},
+ {local_content, true},
+ {attributes, record_info(fields, bosh)}]),
+ mnesia:add_table_copy(bosh, node(), ram_copies).
+
+start_hook_handler() ->
+ %% HACK: We need this in order to avoid
+ %% hooks processing more than once
+ %% when many vhosts are involved
+ %% TODO: get rid of this stuff
+ spawn(fun hook_handler/0).
+
+hook_handler() ->
+ case catch register(ejabberd_bosh_hook_handler, self()) of
+ true ->
+ ejabberd_hooks:add(node_up, ?MODULE, node_up, 100),
+ ejabberd_hooks:add(node_down, ?MODULE, node_down, 100),
+ ejabberd_hooks:add(node_hash_update, ?MODULE, migrate, 100),
+ %% Stop if ejabberd_sup goes down
+ %% (i.e. the whole ejabberd goes down)
+ MRef = erlang:monitor(process, ejabberd_sup),
+ hook_handler_loop(MRef);
+ _ ->
+ ok
+ end.
+
+hook_handler_loop(MRef) ->
+ receive
+ {'DOWN', MRef, _Type, _Object, _Info} ->
+ %% Unregister the hooks. I think this is useless, thus 'catch'
+ catch ejabberd_hooks:delete(node_up, ?MODULE, node_up, 100),
+ catch ejabberd_hooks:delete(node_down, ?MODULE, node_down, 100),
+ catch ejabberd_hooks:delete(node_hash_update, ?MODULE, migrate, 100),
+ ok;
+ _ ->
+ hook_handler_loop(MRef)
+ end.
diff --git a/src/web/mod_http_bind.erl b/src/web/mod_http_bind.erl
index 99409450c..341e9fa7d 100644
--- a/src/web/mod_http_bind.erl
+++ b/src/web/mod_http_bind.erl
@@ -124,7 +124,9 @@ setup_database() ->
migrate_database(),
mnesia:create_table(http_bind,
[{ram_copies, [node()]},
- {attributes, record_info(fields, http_bind)}]).
+ {local_content, true},
+ {attributes, record_info(fields, http_bind)}]),
+ mnesia:add_table_copy(http_bind, node(), ram_copies).
migrate_database() ->
case catch mnesia:table_info(http_bind, attributes) of
@@ -134,4 +136,10 @@ migrate_database() ->
%% Since the stored information is not important, instead
%% of actually migrating data, let's just destroy the table
mnesia:delete_table(http_bind)
+ end,
+ case catch mnesia:table_info(http_bind, local_content) of
+ false ->
+ mnesia:delete_table(http_bind);
+ _ ->
+ ok
end.
diff --git a/src/web/mod_http_bindjson.erl b/src/web/mod_http_bindjson.erl
new file mode 100644
index 000000000..6090ee458
--- /dev/null
+++ b/src/web/mod_http_bindjson.erl
@@ -0,0 +1,156 @@
+%%%----------------------------------------------------------------------
+%%% File : mod_http_bindjson.erl
+%%% Original Author : Stefan Strigler <steve@zeank.in-berlin.de>
+%%% Purpose : Implementation of XMPP over BOSH (XEP-0206)
+%%% Created : Tue Feb 20 13:15:52 CET 2007
+%%%
+%%% ejabberd, Copyright (C) 2002-2010 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+%%%----------------------------------------------------------------------
+%%% This module acts as a bridge to ejabberd_http_bind which implements
+%%% the real stuff, this is to handle the new pluggable architecture for
+%%% extending ejabberd's http service.
+%%%----------------------------------------------------------------------
+%%% I will probable kill and merge code with the original mod_http_bind
+%%% if this feature gains traction.
+%%% Eric Cestari
+
+-module(mod_http_bindjson).
+-author('steve@zeank.in-berlin.de').
+
+%%-define(ejabberd_debug, true).
+
+-behaviour(gen_mod).
+
+-export([
+ start/2,
+ stop/1,
+ process/2
+ ]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("ejabberd_http.hrl").
+-include("http_bind.hrl").
+
+%% Duplicated from ejabberd_http_bind.
+%% TODO: move to hrl file.
+-record(http_bind, {id, pid, to, hold, wait, process_delay, version}).
+
+%%%----------------------------------------------------------------------
+%%% API
+%%%----------------------------------------------------------------------
+
+process([], #request{method = 'POST',
+ data = []}) ->
+ ?DEBUG("Bad Request: no data", []),
+ {400, ?HEADER, {xmlelement, "h1", [],
+ [{xmlcdata, "400 Bad Request"}]}};
+process([], #request{method = 'POST',
+ data = Data,
+ ip = IP}) ->
+ ?DEBUG("Incoming data: ~s", [Data]),
+ %NOTE the whole point of this file is this line.
+ ejabberd_http_bindjson:process_request(Data, IP);
+process([], #request{method = 'GET',
+ data = []}) ->
+ {200, ?HEADER, get_human_html_xmlel()};
+process([], #request{method = 'OPTIONS',
+ data = []}) ->
+ {200, ?OPTIONS_HEADER, []};
+process(_Path, _Request) ->
+ ?DEBUG("Bad Request: ~p", [_Request]),
+ {400, ?HEADER, {xmlelement, "h1", [],
+ [{xmlcdata, "400 Bad Request"}]}}.
+
+get_human_html_xmlel() ->
+ Heading = "ejabberd " ++ atom_to_list(?MODULE),
+ {xmlelement, "html", [{"xmlns", "http://www.w3.org/1999/xhtml"}],
+ [{xmlelement, "head", [],
+ [{xmlelement, "title", [], [{xmlcdata, Heading}]}]},
+ {xmlelement, "body", [],
+ [{xmlelement, "h1", [], [{xmlcdata, Heading}]},
+ {xmlelement, "p", [],
+ [{xmlcdata, "An implementation of "},
+ {xmlelement, "a",
+ [{"href", "http://xmpp.org/extensions/xep-0206.html"}],
+ [{xmlcdata, "XMPP over BOSH (XEP-0206)"}]}]},
+ {xmlelement, "p", [],
+ [{xmlcdata, "This web page is only informative. "
+ "To use HTTP-Bind you need a Jabber/XMPP client that supports it."}
+ ]}
+ ]}]}.
+
+%%%----------------------------------------------------------------------
+%%% BEHAVIOUR CALLBACKS
+%%%----------------------------------------------------------------------
+start(_Host, _Opts) ->
+ setup_database(),
+ HTTPBindSupervisor =
+ {ejabberd_http_bind_sup,
+ {ejabberd_tmp_sup, start_link,
+ [ejabberd_http_bind_sup, ejabberd_http_bind]},
+ permanent,
+ infinity,
+ supervisor,
+ [ejabberd_tmp_sup]},
+ case supervisor:start_child(ejabberd_sup, HTTPBindSupervisor) of
+ {ok, _Pid} ->
+ ok;
+ {ok, _Pid, _Info} ->
+ ok;
+ {error, {already_started, _PidOther}} ->
+ % mod_http_bind is already started so it will not be started again
+ ok;
+ {error, Error} ->
+ {'EXIT', {start_child_error, Error}}
+ end.
+
+stop(_Host) ->
+ case supervisor:terminate_child(ejabberd_sup, ejabberd_http_bind_sup) of
+ ok ->
+ ok;
+ {error, Error} ->
+ {'EXIT', {terminate_child_error, Error}}
+ end.
+
+setup_database() ->
+ migrate_database(),
+ mnesia:create_table(http_bind,
+ [{ram_copies, [node()]},
+ {local_content, true},
+ {attributes, record_info(fields, http_bind)}]),
+ mnesia:add_table_copy(http_bind, node(), ram_copies).
+
+migrate_database() ->
+ case catch mnesia:table_info(http_bind, attributes) of
+ [id, pid, to, hold, wait, process_delay, version] ->
+ ok;
+ _ ->
+ %% Since the stored information is not important, instead
+ %% of actually migrating data, let's just destroy the table
+ mnesia:delete_table(http_bind)
+ end,
+ case catch mnesia:table_info(http_bind, local_content) of
+ false ->
+ mnesia:delete_table(http_bind);
+ _ ->
+ ok
+ end.
diff --git a/src/web/mod_http_fileserver.erl b/src/web/mod_http_fileserver.erl
index 251d367bb..b7c16f1b5 100644
--- a/src/web/mod_http_fileserver.erl
+++ b/src/web/mod_http_fileserver.erl
@@ -26,45 +26,20 @@
-module(mod_http_fileserver).
-author('mmirra@process-one.net').
-
+-author('ecestari@process-one.net').
-behaviour(gen_mod).
--behaviour(gen_server).
%% gen_mod callbacks
-export([start/2, stop/1]).
-%% API
--export([start_link/2]).
-
-%% gen_server callbacks
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
- terminate/2, code_change/3]).
-
%% request_handlers callbacks
-export([process/2]).
-%% ejabberd_hooks callbacks
--export([reopen_log/1]).
-
-include("ejabberd.hrl").
-include("jlib.hrl").
-include_lib("kernel/include/file.hrl").
-%%-include("ejabberd_http.hrl").
-%% TODO: When ejabberd-modules SVN gets the new ejabberd_http.hrl, delete this code:
--record(request, {method,
- path,
- q = [],
- us,
- auth,
- lang = "",
- data = "",
- ip,
- host, % string()
- port, % integer()
- tp, % transfer protocol = http | https
- headers
- }).
+-include("ejabberd_http.hrl").
-ifdef(SSL40).
-define(STRING2LOWER, string).
@@ -76,11 +51,6 @@
-endif.
-endif.
--record(state, {host, docroot, accesslog, accesslogfd, directory_indices,
- custom_headers, default_content_type, content_types = []}).
-
--define(PROCNAME, ejabberd_mod_http_fileserver).
-
%% Response is {DataSize, Code, [{HeaderKey, HeaderValue}], Data}
-define(HTTP_ERR_FILE_NOT_FOUND, {-1, 404, [], "Not found"}).
-define(HTTP_ERR_FORBIDDEN, {-1, 403, [], "Forbidden"}).
@@ -102,81 +72,76 @@
-compile(export_all).
-%%====================================================================
-%% gen_mod callbacks
-%%====================================================================
start(Host, Opts) ->
- Proc = get_proc_name(Host),
- ChildSpec =
- {Proc,
- {?MODULE, start_link, [Host, Opts]},
- transient, % if process crashes abruptly, it gets restarted
- 1000,
- worker,
- [?MODULE]},
- supervisor:start_child(ejabberd_sup, ChildSpec).
-
-stop(Host) ->
- Proc = get_proc_name(Host),
- gen_server:call(Proc, stop),
- supervisor:terminate_child(ejabberd_sup, Proc),
- supervisor:delete_child(ejabberd_sup, Proc).
-
-%%====================================================================
-%% API
-%%====================================================================
-%%--------------------------------------------------------------------
-%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
-%% Description: Starts the server
-%%--------------------------------------------------------------------
-start_link(Host, Opts) ->
- Proc = get_proc_name(Host),
- gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
-
-%%====================================================================
-%% gen_server callbacks
-%%====================================================================
-%%--------------------------------------------------------------------
-%% Function: init(Args) -> {ok, State} |
-%% {ok, State, Timeout} |
-%% ignore |
-%% {stop, Reason}
-%% Description: Initiates the server
-%%--------------------------------------------------------------------
-init([Host, Opts]) ->
- try initialize(Host, Opts) of
- {DocRoot, AccessLog, AccessLogFD, DirectoryIndices,
- CustomHeaders, DefaultContentType, ContentTypes} ->
- {ok, #state{host = Host,
- accesslog = AccessLog,
- accesslogfd = AccessLogFD,
- docroot = DocRoot,
- directory_indices = DirectoryIndices,
- custom_headers = CustomHeaders,
- default_content_type = DefaultContentType,
- content_types = ContentTypes}}
- catch
- throw:Reason ->
- {stop, Reason}
- end.
-
-initialize(Host, Opts) ->
DocRoot = gen_mod:get_opt(docroot, Opts, undefined),
+ set_default_host(Host, Opts),
+ conf_store(Host, docroot, DocRoot),
check_docroot_defined(DocRoot, Host),
DRInfo = check_docroot_exists(DocRoot),
check_docroot_is_dir(DRInfo, DocRoot),
check_docroot_is_readable(DRInfo, DocRoot),
AccessLog = gen_mod:get_opt(accesslog, Opts, undefined),
- AccessLogFD = try_open_log(AccessLog, Host),
+ start_log(Host, AccessLog),
DirectoryIndices = gen_mod:get_opt(directory_indices, Opts, []),
+ conf_store(Host, directory_indices, DirectoryIndices),
+ ServeStaticGzip = gen_mod:get_opt(serve_gzip, Opts, false),
+ conf_store(Host, serve_gzip, ServeStaticGzip),
CustomHeaders = gen_mod:get_opt(custom_headers, Opts, []),
+ conf_store(Host, custom_headers, CustomHeaders),
DefaultContentType = gen_mod:get_opt(default_content_type, Opts,
?DEFAULT_CONTENT_TYPE),
+ conf_store(Host, default_content_type, DefaultContentType),
ContentTypes = build_list_content_types(gen_mod:get_opt(content_types, Opts, []), ?DEFAULT_CONTENT_TYPES),
- ?INFO_MSG("initialize: ~n ~p", [ContentTypes]),%+++
- {DocRoot, AccessLog, AccessLogFD, DirectoryIndices,
- CustomHeaders, DefaultContentType, ContentTypes}.
+ conf_store(Host, content_types, ContentTypes),
+ ?INFO_MSG("initialize: ~n ~p", [ContentTypes]),
+ ok.
+
+% Defines host that will answer request if hostname is not recognized.
+% The first configured host will be used.
+set_default_host(Host, _Opts)->
+ case mochiglobal:get(http_default_host) of
+ undefined ->
+ ?DEBUG("Setting default host to ~p", [Host]),
+ mochiglobal:put(http_default_host, Host);
+ _ ->
+ ok
+ end.
+% Returns current host if it exists or default host
+get_host(Host)->
+ DCT = mochiglobal:get(default_content_type),
+ case lists:keymember(Host, 1, DCT) of
+ true -> Host;
+ false -> mochiglobal:get(http_default_host)
+ end.
+
+conf_store(Host, Key, Value)->
+ R = case mochiglobal:get(Key) of
+ undefined -> [{Host, Value}];
+ A ->
+ case lists:keymember(Host, 1, A) of
+ true -> lists:keyreplace(Host, 1, A,{Host, Value});
+ false -> [{Host, Value}|A]
+ end
+ end,
+ mochiglobal:put(Key, R).
+
+conf_get(Host, Key) ->
+ case mochiglobal:get(Key) of
+ undefined-> undefined;
+ A ->
+ case lists:keyfind(Host, 1, A) of
+ {Host, Val} -> Val;
+ false ->
+ case mochiglobal:get(http_default_host) of
+ Host -> % stop recursion here
+ undefined;
+ DefaultHost ->
+ conf_get(DefaultHost, Key)
+ end
+ end
+ end.
+
%% @spec (AdminCTs::[CT], Default::[CT]) -> [CT]
%% where CT = {Extension::string(), Value}
@@ -184,6 +149,12 @@ initialize(Host, Opts) ->
%% @doc Return a unified list without duplicates.
%% Elements of AdminCTs have more priority.
%% If a CT is declared as 'undefined', then it is not included in the result.
+
+start_log(_Host, undefined)->
+ ok;
+start_log(Host, FileName) ->
+ mod_http_fileserver_log:start(Host, FileName).
+
build_list_content_types(AdminCTsUnsorted, DefaultCTsUnsorted) ->
AdminCTs = lists:ukeysort(1, AdminCTsUnsorted),
DefaultCTs = lists:ukeysort(1, DefaultCTsUnsorted),
@@ -214,79 +185,10 @@ check_docroot_is_readable(DRInfo, DocRoot) ->
read_write -> ok;
_ -> throw({docroot_not_readable, DocRoot})
end.
-
-try_open_log(undefined, _Host) ->
- undefined;
-try_open_log(FN, Host) ->
- FD = try open_log(FN) of
- FD1 -> FD1
- catch
- throw:{cannot_open_accesslog, FN, Reason} ->
- ?ERROR_MSG("Cannot open access log file: ~p~nReason: ~p", [FN, Reason]),
- undefined
- end,
- ejabberd_hooks:add(reopen_log_hook, Host, ?MODULE, reopen_log, 50),
- FD.
-
-%%--------------------------------------------------------------------
-%% 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({serve, LocalPath}, _From, State) ->
- Reply = serve(LocalPath, State#state.docroot, State#state.directory_indices,
- State#state.custom_headers,
- State#state.default_content_type, State#state.content_types),
- {reply, Reply, State};
-handle_call(_Request, _From, State) ->
- {reply, ok, State}.
-
-%%--------------------------------------------------------------------
-%% Function: handle_cast(Msg, State) -> {noreply, State} |
-%% {noreply, State, Timeout} |
-%% {stop, Reason, State}
-%% Description: Handling cast messages
-%%--------------------------------------------------------------------
-handle_cast({add_to_log, FileSize, Code, Request}, State) ->
- add_to_log(State#state.accesslogfd, FileSize, Code, Request),
- {noreply, State};
-handle_cast(reopen_log, State) ->
- FD2 = reopen_log(State#state.accesslog, State#state.accesslogfd),
- {noreply, State#state{accesslogfd = FD2}};
-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(_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) ->
- close_log(State#state.accesslogfd),
- ejabberd_hooks:delete(reopen_log_hook, State#state.host, ?MODULE, reopen_log, 50),
+
+stop(_Host) ->
ok.
-%%--------------------------------------------------------------------
-%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
-%% Description: Convert process state when code is changed
-%%--------------------------------------------------------------------
-code_change(_OldVsn, State, _Extra) ->
- {ok, State}.
%%====================================================================
%% request_handlers callbacks
@@ -296,114 +198,133 @@ code_change(_OldVsn, State, _Extra) ->
%% @doc Handle an HTTP request.
%% LocalPath is the part of the requested URL path that is "local to the module".
%% Returns the page to be sent back to the client and/or HTTP status code.
+
process(LocalPath, Request) ->
?DEBUG("Requested ~p", [LocalPath]),
- try gen_server:call(get_proc_name(Request#request.host), {serve, LocalPath}) of
- {FileSize, Code, Headers, Contents} ->
- add_to_log(FileSize, Code, Request),
- {Code, Headers, Contents}
- catch
- exit:{noproc, _} ->
- ?ERROR_MSG("Received an HTTP request with Host ~p, but couldn't find the related "
- "ejabberd virtual host", [Request#request.host]),
- ejabberd_web:error(not_found)
- end.
-
-serve(LocalPath, DocRoot, DirectoryIndices, CustomHeaders, DefaultContentType, ContentTypes) ->
+ Host = get_host(Request#request.host),
+ ClientHeaders = Request#request.headers,
+ DirectoryIndices = conf_get(Host, directory_indices),
+ CustomHeaders = conf_get(Host, custom_headers),
+ DefaultContentType = conf_get(Host, default_content_type),
+ ContentTypes = conf_get(Host, content_types),
+ Encoding = conf_get(Host, serve_gzip),
+ Static = select_encoding(ClientHeaders, Encoding),
+ DocRoot = conf_get(Host, docroot),
FileName = filename:join(filename:split(DocRoot) ++ LocalPath),
- case file:read_file_info(FileName) of
+ {FileSize, Code, Headers, Contents} = case file:read_file_info(FileName) of
{error, enoent} -> ?HTTP_ERR_FILE_NOT_FOUND;
{error, eacces} -> ?HTTP_ERR_FORBIDDEN;
{ok, #file_info{type = directory}} -> serve_index(FileName,
DirectoryIndices,
CustomHeaders,
DefaultContentType,
- ContentTypes);
- {ok, FileInfo} -> serve_file(FileInfo, FileName,
- CustomHeaders,
- DefaultContentType,
- ContentTypes)
+ ContentTypes, Static);
+ {ok, FileInfo} ->
+ case should_serve(FileInfo, ClientHeaders) of
+ true ->serve_file(FileInfo, FileName,
+ CustomHeaders,
+ DefaultContentType,
+ ContentTypes, Static);
+ false ->
+ {0, 304, [], []}
+ end
+ end,
+ mod_http_fileserver_log:add_to_log(Host,FileSize, Code, Request),
+ {Code, Headers, Contents}.
+
+should_serve(FileInfo, Headers) ->
+ lists:foldl(fun({Header, Fun}, Acc)->
+ case lists:keyfind(Header, 1, Headers) of
+ {_, Val} ->
+ Fun(FileInfo,Val);
+ _O ->
+ Acc
+ end
+ end, true, [{'If-None-Match',fun etag/2}
+ ]).
+etag(FileInfo, Etag)->
+ case httpd_util:create_etag(FileInfo) of
+ Etag ->
+ false;
+ _ ->
+ true
+ end.
+modified(FileInfo, LastModified)->
+ AfterDate = calendar:datetime_to_gregorian_seconds(
+ httpd_util:convert_request_date(LastModified)),
+ Mtime = calendar:datetime_to_gregorian_seconds(FileInfo#file_info.mtime),
+ ?DEBUG("Modified : ~p > ~p (serving: ~p)", [Mtime, AfterDate,Mtime > AfterDate]),
+ Mtime > AfterDate.
+
+select_encoding(_Headers, false)->
+ false;
+select_encoding(Headers, Configuration)->
+ Value = find_header('Accept-Encoding', Headers, ""),
+ Schemes = string:tokens(Value, ","),
+ case lists:member("gzip",Schemes) of
+ true -> Configuration;
+ false -> false
end.
%% Troll through the directory indices attempting to find one which
%% works, if none can be found, return a 404.
-serve_index(_FileName, [], _CH, _DefaultContentType, _ContentTypes) ->
+serve_index(_FileName, [], _CH, _DefaultContentType, _ContentTypes, _Static) ->
?HTTP_ERR_FILE_NOT_FOUND;
-serve_index(FileName, [Index | T], CH, DefaultContentType, ContentTypes) ->
+serve_index(FileName, [Index | T], CH, DefaultContentType, ContentTypes, Static) ->
IndexFileName = filename:join([FileName] ++ [Index]),
case file:read_file_info(IndexFileName) of
- {error, _Error} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes);
- {ok, #file_info{type = directory}} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes);
- {ok, FileInfo} -> serve_file(FileInfo, IndexFileName, CH, DefaultContentType, ContentTypes)
+ {error, _Error} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes, Static);
+ {ok, #file_info{type = directory}} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes, Static);
+ {ok, FileInfo} -> serve_file(FileInfo, IndexFileName, CH, DefaultContentType, ContentTypes, Static)
end.
%% Assume the file exists if we got this far and attempt to read it in
%% and serve it up.
-serve_file(FileInfo, FileName, CustomHeaders, DefaultContentType, ContentTypes) ->
+
+serve_file(FileInfo, FileName, CustomHeaders, DefaultContentType, ContentTypes, false) ->
?DEBUG("Delivering: ~s", [FileName]),
- {ok, FileContents} = file:read_file(FileName),
ContentType = content_type(FileName, DefaultContentType, ContentTypes),
+ {ok, FileContents} = file:read_file(FileName),
{FileInfo#file_info.size,
200, [{"Server", "ejabberd"},
{"Last-Modified", last_modified(FileInfo)},
{"Content-Type", ContentType} | CustomHeaders],
- FileContents}.
-
-%%----------------------------------------------------------------------
-%% Log file
-%%----------------------------------------------------------------------
-
-open_log(FN) ->
- case file:open(FN, [append]) of
- {ok, FD} ->
- FD;
- {error, Reason} ->
- throw({cannot_open_accesslog, FN, Reason})
+ FileContents};
+
+serve_file(FileInfo, FileName, CustomHeaders, DefaultContentType, ContentTypes, Gzip) ->
+ ?DEBUG("Delivering: ~s", [FileName]),
+ ContentType = content_type(FileName, DefaultContentType, ContentTypes),
+ CompressedFileName = FileName ++ ".gz",
+ case file:read_file_info(CompressedFileName) of
+ {ok, FileInfoCompressed} -> %Found compressed
+ ?INFO_MSG("Found compressed: ~s", [FileName]),
+ {ok, FileContents} = file:read_file(CompressedFileName),
+ {FileInfoCompressed#file_info.size,
+ 200, [{"Server", "ejabberd"},
+ {"Last-Modified", last_modified(FileInfoCompressed)},
+ {"Content-Type", ContentType},
+ {"Etag", httpd_util:create_etag(FileInfoCompressed)},
+ {"Content-Encoding", "gzip"} | CustomHeaders],
+ FileContents};
+ {error, _} ->
+ {FileContents, Size} = case Gzip of
+ static ->
+ {ok, Content} = file:read_file(FileName),
+ {Content, FileInfo#file_info.size};
+ always ->
+ {ok, Content} = file:read_file(FileName),
+ Compressed = zlib:gzip(Content),
+ {Compressed, size(Compressed)}
+ end,
+ {Size,
+ 200, [{"Server", "ejabberd"},
+ {"Last-Modified", last_modified(FileInfo)},
+ {"Etag", httpd_util:create_etag(FileInfo)},
+ {"Content-Type", ContentType},
+ {"Content-Encoding", "gzip"} | CustomHeaders],
+ FileContents}
end.
-close_log(FD) ->
- file:close(FD).
-
-reopen_log(undefined, undefined) ->
- ok;
-reopen_log(FN, FD) ->
- close_log(FD),
- open_log(FN).
-
-reopen_log(Host) ->
- gen_server:cast(get_proc_name(Host), reopen_log).
-
-add_to_log(FileSize, Code, Request) ->
- gen_server:cast(get_proc_name(Request#request.host),
- {add_to_log, FileSize, Code, Request}).
-
-add_to_log(undefined, _FileSize, _Code, _Request) ->
- ok;
-add_to_log(File, FileSize, Code, Request) ->
- {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(),
- IP = ip_to_string(element(1, Request#request.ip)),
- Path = join(Request#request.path, "/"),
- Query = case join(lists:map(fun(E) -> lists:concat([element(1, E), "=", element(2, E)]) end,
- Request#request.q), "&") of
- [] ->
- "";
- String ->
- [$? | String]
- end,
- UserAgent = find_header('User-Agent', Request#request.headers, "-"),
- Referer = find_header('Referer', Request#request.headers, "-"),
- %% Pseudo Combined Apache log format:
- %% 127.0.0.1 - - [28/Mar/2007:18:41:55 +0200] "GET / HTTP/1.1" 302 303 "-" "tsung"
- %% TODO some fields are harcoded/missing:
- %% The date/time integers should have always 2 digits. For example day "7" should be "07"
- %% Month should be 3*letter, not integer 1..12
- %% Missing time zone = (`+' | `-') 4*digit
- %% Missing protocol version: HTTP/1.1
- %% For reference: http://httpd.apache.org/docs/2.2/logs.html
- io:format(File, "~s - - [~p/~p/~p:~p:~p:~p] \"~s /~s~s\" ~p ~p ~p ~p~n",
- [IP, Day, Month, Year, Hour, Minute, Second, Request#request.method, Path, Query, Code,
- FileSize, Referer, UserAgent]).
-
find_header(Header, Headers, Default) ->
case lists:keysearch(Header, 1, Headers) of
{value, {_, Value}} -> Value;
@@ -414,15 +335,6 @@ find_header(Header, Headers, Default) ->
%% Utilities
%%----------------------------------------------------------------------
-get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?PROCNAME).
-
-join([], _) ->
- "";
-join([E], _) ->
- E;
-join([H | T], Separator) ->
- lists:foldl(fun(E, Acc) -> lists:concat([Acc, Separator, E]) end, H, T).
-
content_type(Filename, DefaultContentType, ContentTypes) ->
Extension = ?STRING2LOWER:to_lower(filename:extension(Filename)),
case lists:keysearch(Extension, 1, ContentTypes) of
@@ -433,11 +345,3 @@ content_type(Filename, DefaultContentType, ContentTypes) ->
last_modified(FileInfo) ->
Then = FileInfo#file_info.mtime,
httpd_util:rfc1123_date(Then).
-
-%% Convert IP address tuple to string representation. Accepts either
-%% IPv4 or IPv6 address tuples.
-ip_to_string(Address) when size(Address) == 4 ->
- join(tuple_to_list(Address), ".");
-ip_to_string(Address) when size(Address) == 8 ->
- Parts = lists:map(fun (Int) -> io_lib:format("~.16B", [Int]) end, tuple_to_list(Address)),
- ?STRING2LOWER:to_lower(lists:flatten(join(Parts, ":"))).
diff --git a/src/web/mod_http_fileserver_log.erl b/src/web/mod_http_fileserver_log.erl
new file mode 100644
index 000000000..b76f947c8
--- /dev/null
+++ b/src/web/mod_http_fileserver_log.erl
@@ -0,0 +1,167 @@
+-module (mod_http_fileserver_log).
+
+-behaviour (gen_server).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+
+-export ([start_link/2,start/2, stop/1, add_to_log/4,reopen_log/1]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("ejabberd_http.hrl").
+-include_lib("kernel/include/file.hrl").
+
+-define(PROCNAME, ejabberd_mod_http_fileserver_log).
+
+-record(state, {host,accesslog, accesslogfd}).
+%% Public API
+
+start(Host, Filename) ->
+ Proc =gen_mod:get_module_proc(Host, ?PROCNAME),
+ ChildSpec =
+ {Proc,
+ {?MODULE, start_link, [Host, Filename]},
+ transient, % if process crashes abruptly, it gets restarted
+ 1000,
+ worker,
+ [?MODULE]},
+ supervisor:start_child(ejabberd_sup, ChildSpec).
+
+stop(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:call(Proc, stop),
+ supervisor:terminate_child(ejabberd_sup, Proc),
+ supervisor:delete_child(ejabberd_sup, Proc).
+
+start_link(Host, Filename) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Filename], []).
+
+add_to_log(Host,FileSize, Code, Request) ->
+ gen_server:cast(gen_mod:get_module_proc(Host, ?PROCNAME),
+ {add_to_log, FileSize, Code, Request}).
+
+reopen_log(Host) ->
+ gen_server:cast(gen_mod:get_module_proc(Host, ?PROCNAME), reopen_log).
+
+%% Server implementation, a.k.a.: callbacks
+
+init([Host, Filename]) ->
+ try try_open_log(Filename, Host) of
+ AccessLogFD ->
+ ?DEBUG("File opened !", []),
+ {ok, #state{host = Host,
+ accesslog = Filename,
+ accesslogfd = AccessLogFD}}
+ catch
+ throw:Reason ->
+ {stop, Reason}
+ end.
+
+try_open_log(FN, Host) ->
+ FD = try open_log(FN) of
+ FD1 -> FD1
+ catch
+ throw:{cannot_open_accesslog, FN, Reason} ->
+ ?ERROR_MSG("Cannot open access log file: ~p~nReason: ~p", [FN, Reason]),
+ undefined
+ end,
+ %HostB = list_to_binary(Host),
+ ejabberd_hooks:add(reopen_log_hook, Host, ?MODULE, reopen_log, 50),
+ FD.
+
+handle_call(_Request, _From, State) ->
+ {reply, ok, State}.
+
+handle_cast({add_to_log, FileSize, Code, Request}, State) ->
+ add_to_log2(State#state.accesslogfd, FileSize, Code, Request),
+ {noreply, State};
+handle_cast(reopen_log, State) ->
+ FD2 = reopen_log(State#state.accesslog, State#state.accesslogfd),
+ {noreply, State#state{accesslogfd = FD2}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ close_log(State#state.accesslogfd),
+ ejabberd_hooks:delete(reopen_log_hook, State#state.host, ?MODULE, reopen_log, 50),
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+
+%%----------------------------------------------------------------------
+%% Log file
+%%----------------------------------------------------------------------
+
+open_log(FN) ->
+ case file:open(FN, [append]) of
+ {ok, FD} ->
+ FD;
+ {error, Reason} ->
+ throw({cannot_open_accesslog, FN, Reason})
+ end.
+
+close_log(FD) ->
+ file:close(FD).
+
+reopen_log(undefined, undefined) ->
+ ok;
+reopen_log(FN, FD) ->
+ ?DEBUG("reopening logs", []),
+ close_log(FD),
+ open_log(FN).
+
+
+
+add_to_log2(undefined, _FileSize, _Code, _Request) ->
+ ok;
+add_to_log2(File, FileSize, Code, Request) ->
+ {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(),
+ IP = ip_to_string(element(1, Request#request.ip)),
+ Path = join(Request#request.path, "/"),
+ Query = case join(lists:map(fun(E) -> lists:concat([element(1, E), "=", element(2, E)]) end,
+ Request#request.q), "&") of
+ [] ->
+ "";
+ String ->
+ [$? | String]
+ end,
+ UserAgent = find_header('User-Agent', Request#request.headers, "-"),
+ Referer = find_header('Referer', Request#request.headers, "-"),
+ %% Pseudo Combined Apache log format:
+ %% 127.0.0.1 - - [28/Mar/2007:18:41:55 +0200] "GET / HTTP/1.1" 302 303 "-" "tsung"
+ %% TODO some fields are harcoded/missing:
+ %% The date/time integers should have always 2 digits. For example day "7" should be "07"
+ %% Month should be 3*letter, not integer 1..12
+ %% Missing time zone = (`+' | `-') 4*digit
+ %% Missing protocol version: HTTP/1.1
+ %% For reference: http://httpd.apache.org/docs/2.2/logs.html
+ io:format(File, "~s - - [~p/~p/~p:~p:~p:~p] \"~s /~s~s\" ~p ~p ~p ~p~n",
+ [IP, Day, Month, Year, Hour, Minute, Second, Request#request.method, Path, Query, Code,
+ FileSize, Referer, UserAgent]).
+
+find_header(Header, Headers, Default) ->
+ case lists:keysearch(Header, 1, Headers) of
+ {value, {_, Value}} -> Value;
+ false -> Default
+ end.
+
+join([], _) ->
+ "";
+join([E], _) ->
+ E;
+join([H | T], Separator) ->
+ lists:foldl(fun(E, Acc) -> lists:concat([Acc, Separator, E]) end, H, T).
+
+%% Convert IP address tuple to string representation. Accepts either
+%% IPv4 or IPv6 address tuples.
+ip_to_string(Address) when size(Address) == 4 ->
+ join(tuple_to_list(Address), ".");
+ip_to_string(Address) when size(Address) == 8 ->
+ Parts = lists:map(fun (Int) -> io_lib:format("~.16B", [Int]) end, tuple_to_list(Address)),
+ string:to_lower(lists:flatten(join(Parts, ":"))). \ No newline at end of file
diff --git a/src/web/pshb_http.erl b/src/web/pshb_http.erl
new file mode 100644
index 000000000..02eb0a4cc
--- /dev/null
+++ b/src/web/pshb_http.erl
@@ -0,0 +1,416 @@
+%%%----------------------------------------------------------------------
+%%% File : pshb_http.erl
+%%% Author : Eric Cestari <ecestari@process-one.net>
+%%% Purpose :
+%%% Created :01-09-2010
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2010 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+%%%
+%%% {5280, ejabberd_http, [
+%%% http_poll,
+%%% web_admin,
+%%% {request_handlers, [{["pshb"], pshb_http}]} % this should be added
+%%% ]}
+%%%
+%%% To post to a node the content of the file "sam.atom" on the "foo", on the localhost virtual host, using cstar@localhost
+%%% curl -u cstar@localhost:encore -i -X POST http://localhost:5280/pshb/localhost/foo -d @sam.atom
+%%%
+
+
+-module (pshb_http).
+-author('ecestari@process-one.net').
+
+-compile({no_auto_import,[error/1]}).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("ejabberd_http.hrl").
+
+-include("mod_pubsub/pubsub.hrl").
+
+-export([process/2]).
+
+process([Domain | _Rest] = LocalPath, #request{auth = Auth} = Request)->
+ UD = get_auth(Auth),
+ Module = backend(Domain),
+ case catch out(Module, Request, Request#request.method, LocalPath,UD) of
+ {'EXIT', Error} ->
+ ?ERROR_MSG("Error while processing ~p : ~n~p", [LocalPath, Error]),
+ error(500);
+ Result ->
+ Result
+ end.
+
+get_auth(Auth) ->
+ case Auth of
+ {SJID, P} ->
+ case jlib:string_to_jid(SJID) of
+ error ->
+ undefined;
+ #jid{user = U, server = S} ->
+ case ejabberd_auth:check_password(U, S, P) of
+ true ->
+ {U, S};
+ false ->
+ undefined
+ end
+ end;
+ _ ->
+ undefined
+ end.
+
+out(Module, Args, 'GET', [Domain,Node]=Uri, _User) ->
+ case Module:tree_action(get_host(Uri), get_node, [get_host(Uri),get_collection(Uri)]) of
+ {error, Error} ->
+ error(Error);
+ #pubsub_node{options = Options}->
+ AccessModel = lists:keyfind(access_model, 1, Options),
+ case AccessModel of
+ {access_model, open} ->
+ Items = lists:sort(fun(X,Y)->
+ {DateX, _} = X#pubsub_item.modification,
+ {DateY, _} = Y#pubsub_item.modification,
+ DateX > DateY
+ end, Module:get_items(
+ get_host(Uri),
+ get_collection(Uri))),
+ case Items of
+ [] -> ?DEBUG("Items : ~p ~n", [collection(get_collection(Uri),
+ collection_uri(Args,Domain,Node), calendar:now_to_universal_time(erlang:now()), "", [])]),
+ {200, [{"Content-Type", "application/atom+xml"}],
+ collection(get_collection(Uri),
+ collection_uri(Args,Domain,Node), calendar:now_to_universal_time(erlang:now()), "", [])};
+ _ ->
+ #pubsub_item{modification = {LastDate, _JID}} = LastItem = hd(Items),
+ Etag =generate_etag(LastItem),
+ IfNoneMatch=proplists:get_value('If-None-Match', Args#request.headers),
+ if IfNoneMatch==Etag
+ ->
+ success(304);
+ true ->
+ XMLEntries= [item_to_entry(Args,Domain, Node,Entry)||Entry <- Items],
+ {200, [{"Content-Type", "application/atom+xml"},{"Etag", Etag}],
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ ++ xml:element_to_string(
+ collection(get_collection(Uri), collection_uri(Args,Domain,Node),
+ calendar:now_to_universal_time(LastDate), "", XMLEntries))}
+ end
+ end;
+ {access_model, Access} ->
+ ?INFO_MSG("Uri ~p requested. access_model is ~p. HTTP access denied unless access_model =:= open",
+ [Uri, Access]),
+ error(?ERR_FORBIDDEN)
+ end
+ end;
+
+out(Module, Args, 'POST', [_D, _Node]=Uri, {_User, _Domain} = UD) ->
+ publish_item(Module, Args, Uri, uniqid(false), UD);
+
+out(Module, Args, 'PUT', [_D, _Node, Slug]=Uri, {_User, _Domain} = UD) ->
+ publish_item(Module, Args, Uri, Slug, UD);
+
+out(Module, _Args, 'DELETE', [_D, Node, Id]= Uri, {User, UDomain}) ->
+ Jid = jlib:make_jid({User, UDomain, ""}),
+ case Module:delete_item(get_host(Uri), list_to_binary(Node), Jid, Id) of
+ {error, Error} -> error(Error);
+ {result, _Res} -> success(200)
+ end;
+
+
+out(Module, Args, 'PUT', [_Domain, Node]= Uri, {User, UDomain}) ->
+ Host = get_host(Uri),
+ Jid = jlib:make_jid({User, UDomain, ""}),
+ Payload = xml_stream:parse_element(Args#request.data),
+ ConfigureElement = case xml:get_subtag(Payload, "configure") of
+ false ->[];
+ {xmlelement, _, _, SubEls}->SubEls
+ end,
+ case Module:set_configure(Host, list_to_binary(Node), Jid, ConfigureElement, Args#request.lang) of
+ {result, []} -> success(200);
+ {error, Error} -> error(Error)
+ end;
+
+out(Module, Args, 'GET', [Domain]=Uri, From)->
+ Host = get_host(Uri),
+ ?DEBUG("Host = ~p", [Host]),
+ case Module:tree_action(Host, get_subnodes, [Host, <<>>, From ]) of
+ [] ->
+ ?DEBUG("Error getting URI ~p : ~p",[Uri, From]),
+ error(?ERR_ITEM_NOT_FOUND);
+ Collections ->
+ {200, [{"Content-Type", "application/atomsvc+xml"}], "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ ++ xml:element_to_string(service(Args,Domain, Collections))}
+ end;
+
+out(Module, Args, 'POST', [Domain]=Uri, {User, UDomain})->
+ Host = get_host(Uri),
+ Payload = xml_stream:parse_element(Args#request.data),
+ {Node, Type} = case xml:get_subtag(Payload, "create") of
+ false -> {<<>>,"flat"};
+ E ->
+ {list_to_binary(get_tag_attr_or_default("node", E,"")),
+ get_tag_attr_or_default("type", E,"flat")}
+ end,
+ ConfigureElement = case xml:get_subtag(Payload, "configure") of
+ false ->[];
+ {xmlelement, _, _, SubEls}->SubEls
+ end,
+ Jid = jlib:make_jid({User, UDomain, ""}),
+ case Module:create_node(Host, Domain, Node, Jid, Type, all, ConfigureElement) of
+ {error, Error} ->
+ ?ERROR_MSG("Error create node via HTTP : ~p",[Error]),
+ error(Error); % will probably detail more
+ {result, [Result]} ->
+ {200, [{"Content-Type", "application/xml"}], "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ ++ xml:element_to_string(Result)}
+ end;
+
+out(Module,_Args, 'DELETE', [_Domain, Node] = Uri, {User, UDomain})->
+ Host = get_host(Uri),
+ Jid = jlib:make_jid({User, UDomain, ""}),
+ BinNode = list_to_binary(Node),
+ case Module:delete_node(Host, BinNode, Jid) of
+ {error, Error} -> error(Error);
+ {result, []} ->
+ {200, [],[]}
+ end;
+
+
+
+out(Module, Args, 'GET', [Domain, Node, _Item]=URI, _) ->
+ Failure = fun(Error)->
+ ?DEBUG("Error getting URI ~p : ~p",[URI, Error]),
+ error(Error)
+ end,
+ Success = fun(Item)->
+ Etag =generate_etag(Item),
+ IfNoneMatch=proplists:get_value('If-None-Match', Args#request.headers),
+ if IfNoneMatch==Etag
+ ->
+ success(304);
+ true ->
+ {200, [{"Content-Type", "application/atom+xml"},{"Etag", Etag}], "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ ++ xml:element_to_string(item_to_entry(Args, Domain,Node, Item))}
+ end
+ end,
+ get_item(Module, URI, Failure, Success);
+
+out(_Module,_,Method,Uri,undefined) ->
+ ?DEBUG("Error, ~p not authorized for ~p : ~p",[ Method,Uri]),
+ error(?ERR_FORBIDDEN).
+
+get_item(Module, Uri, Failure, Success)->
+ ?DEBUG(" Module:get_item(~p, ~p,~p)", [get_host(Uri), get_collection(Uri), get_member(Uri)]),
+ case Module:get_item(get_host(Uri), get_collection(Uri), get_member(Uri)) of
+ {error, Reason} ->
+ Failure(Reason);
+ #pubsub_item{}=Item ->
+ Success(Item)
+ end.
+
+publish_item(Module, Args, [Domain, Node | _R] = Uri, Slug, {User, Domain})->
+
+ Payload = xml_stream:parse_element(Args#request.data),
+ [FilteredPayload]=xml:remove_cdata([Payload]),
+
+ %FilteredPayload2 = case xml:get_subtag(FilteredPayload, "app:edited") ->
+ % {xmlelement, Name, Attrs, [{cdata, }]}
+ case Module:publish_item(get_host(Uri),
+ Domain,
+ get_collection(Uri),
+ jlib:make_jid(User,Domain, ""),
+ Slug,
+ [FilteredPayload]) of
+ {result, [_]} ->
+ ?DEBUG("Publishing to ~p~n",[entry_uri(Args, Domain, Node,Slug)]),
+ {201, [{"location", entry_uri(Args, Domain,Node,Slug)}], Payload};
+ {error, Error} ->
+ error(Error)
+ end.
+
+generate_etag(#pubsub_item{modification={{_, D2, D3}, _JID}})->integer_to_list(D3+D2).
+get_host([Domain|_Rest])-> "pubsub."++Domain.
+get_collection([_Domain, Node|_Rest])->list_to_binary(Node).
+get_member([_Domain, _Node, Member])->
+ Member.
+
+collection_uri(R, Domain, Node) ->
+ base_uri(R, Domain)++ "/" ++ b2l(Node).
+
+entry_uri(R,Domain, Node, Id)->
+ collection_uri(R,Domain, Node)++"/"++b2l(Id).
+
+base_uri(#request{host=Host, port=Port}, Domain)->
+ "http://"++Host++":"++i2l(Port)++"/pshb/" ++ Domain.
+
+item_to_entry(Args,Domain, Node,#pubsub_item{itemid={Id,_}, payload=Entry}=Item)->
+ [R]=xml:remove_cdata(Entry),
+ item_to_entry(Args, Domain, Node, Id, R, Item).
+
+item_to_entry(Args,Domain, Node, Id,{xmlelement, "entry", Attrs, SubEl},
+ #pubsub_item{modification={ Secs, JID} }) ->
+ Date = calendar:now_to_local_time(Secs),
+ {_User, Domain, _}=jlib:jid_tolower(JID),
+ SubEl2=[{xmlelement, "app:edited", [], [{xmlcdata, w3cdtf(Date)}]},
+ {xmlelement, "updated", [],[{xmlcdata, w3cdtf(Date)}]},
+ {xmlelement, "author", [],[{xmlelement, "name", [], [{xmlcdata, list_to_binary(jlib:jid_to_string(JID))}]}]},
+ {xmlelement, "link",[{"rel", "edit"},
+ {"href", entry_uri(Args,Domain,Node, Id)}],[] },
+ {xmlelement, "id", [],[{xmlcdata, entry_uri(Args, Domain, Node, Id)}]}
+ | SubEl],
+ {xmlelement, "entry", [{"xmlns:app","http://www.w3.org/2007/app"}|Attrs], SubEl2};
+
+% Don't do anything except adding xmlns
+item_to_entry(_Args,_Domain, Node, _Id, {xmlelement, Name, Attrs, Subels}=Element, _Item)->
+ case proplists:is_defined("xmlns",Attrs) of
+ true -> Element;
+ false -> {xmlelement, Name, [{"xmlns", Node}|Attrs], Subels}
+ end.
+
+collection(Title, Link, Updated, _Id, Entries)->
+ {xmlelement, "feed", [{"xmlns", "http://www.w3.org/2005/Atom"},
+ {"xmlns:app", "http://www.w3.org/2007/app"}], [
+ {xmlelement, "title", [],[{xmlcdata, Title}]},
+ {xmlelement, "generator", [],[{xmlcdata, <<"ejabberd">>}]},
+ {xmlelement, "updated", [],[{xmlcdata, w3cdtf(Updated)}]},
+ {xmlelement, "link", [{"href", Link}, {"rel", "self"}], []},
+ {xmlelement, "id", [], [{xmlcdata, list_to_binary(Link)}]},
+ {xmlelement, "title", [],[{xmlcdata, Title}]} |
+ Entries
+ ]}.
+
+service(Args, Domain,Collections)->
+ {xmlelement, "service", [{"xmlns", "http://www.w3.org/2007/app"},
+ {"xmlns:atom", "http://www.w3.org/2005/Atom"},
+ {"xmlns:app", "http://www.w3.org/2007/app"}],[
+ {xmlelement, "workspace", [],[
+ {xmlelement, "atom:title", [],[{xmlcdata,"Pubsub node Feed for " ++Domain}]} |
+ lists:map(fun(#pubsub_node{nodeid={_Server, Id}, type=_Type})->
+ {xmlelement, "collection", [{"href", collection_uri(Args,Domain, Id)}], [
+ {xmlelement, "atom:title", [], [{xmlcdata, Id}]}
+ ]}
+ end, Collections)
+ ]}
+ ]}.
+
+%% simple output functions
+error({xmlelement, "error", Attrs, _}=Error) ->
+ Value = list_to_integer(xml:get_attr_s("code", Attrs)),
+ {Value, [{"Content-type", "application/xml"}], xml:element_to_string(Error)};
+error(404)->
+ {404, [], "Not Found"};
+error(403)->
+ {403, [], "Forbidden"};
+error(500)->
+ {500, [], "Internal server error"};
+error(401)->
+ {401, [{"WWW-Authenticate", "basic realm=\"ejabberd\""}],"Unauthorized"};
+error(Code)->
+ {Code, [], ""}.
+success(200)->
+ {200, [], ""};
+success(Code)->
+ {Code, [], ""}.
+
+backend(Domain)->
+ Modules = gen_mod:loaded_modules(Domain),
+ case lists:member(mod_pubsub_odbc, Modules) of
+ true -> mod_pubsub_odbc;
+ _ -> mod_pubsub
+ end.
+
+
+% Code below is taken (with some modifications) from the yaws webserver, which
+% is distributed under the folowing license:
+%
+% This software (the yaws webserver) is free software.
+% Parts of this software is Copyright (c) Claes Wikstrom <klacke@hyber.org>
+% Any use or misuse of the source code is hereby freely allowed.
+%
+% 1. Redistributions of source code must retain the above copyright
+% notice as well as this list of conditions.
+%
+% 2. Redistributions in binary form must reproduce the above copyright
+% notice as well as this list of conditions.
+%%% Create W3CDTF (http://www.w3.org/TR/NOTE-datetime) formatted date
+%%% w3cdtf(GregSecs) -> "YYYY-MM-DDThh:mm:ssTZD"
+%%%
+uniqid(false)->
+ {T1, T2, T3} = now(),
+ lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3]));
+uniqid(Slug) ->
+ Slut = string:to_lower(Slug),
+ S = string:substr(Slut, 1, 9),
+ {_T1, T2, T3} = now(),
+ lists:flatten(io_lib:fwrite("~s-~.16B~.16B", [S, T2, T3])).
+
+w3cdtf(Date) -> %1 Date = calendar:gregorian_seconds_to_datetime(GregSecs),
+ {{Y, Mo, D},{H, Mi, S}} = Date,
+ [UDate|_] = calendar:local_time_to_universal_time_dst(Date),
+ {DiffD,{DiffH,DiffMi,_}}=calendar:time_difference(UDate,Date),
+ w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi).
+
+%%% w3cdtf's helper function
+w3cdtf_diff(Y, Mo, D, H, Mi, S, _DiffD, DiffH, DiffMi) when DiffH < 12, DiffH /= 0 ->
+ i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++
+ add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++
+ add_zero(S) ++ "+" ++ add_zero(DiffH) ++ ":" ++ add_zero(DiffMi);
+
+w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD == 0 ->
+ i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++
+ add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++
+ add_zero(S) ++ "+" ++ add_zero(DiffH) ++ ":" ++
+ add_zero(DiffMi);
+
+w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD /= 0, DiffMi /= 0 ->
+ i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++
+ add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++
+ add_zero(S) ++ "-" ++ add_zero(23-DiffH) ++
+ ":" ++ add_zero(60-DiffMi);
+
+w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD /= 0, DiffMi == 0 ->
+ i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++
+ add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++
+ add_zero(S) ++ "-" ++ add_zero(24-DiffH) ++
+ ":" ++ add_zero(DiffMi);
+
+w3cdtf_diff(Y, Mo, D, H, Mi, S, _DiffD, DiffH, _DiffMi) when DiffH == 0 ->
+ i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++
+ add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++
+ add_zero(S) ++ "Z".
+
+add_zero(I) when is_integer(I) -> add_zero(i2l(I));
+add_zero([A]) -> [$0,A];
+add_zero(L) when is_list(L) -> L.
+
+
+
+i2l(I) when is_integer(I) -> integer_to_list(I);
+i2l(L) when is_list(L) -> L.
+
+b2l(B) when is_binary(B) -> binary_to_list(B);
+b2l(L) when is_list(L) -> L.
+
+get_tag_attr_or_default(AttrName, Element, Default)->
+ case xml:get_tag_attr_s(AttrName, Element) of
+ "" -> Default;
+ Val -> Val
+ end.
diff --git a/src/web/simple_ws_check.erl b/src/web/simple_ws_check.erl
new file mode 100644
index 000000000..8ef160980
--- /dev/null
+++ b/src/web/simple_ws_check.erl
@@ -0,0 +1,11 @@
+-module (simple_ws_check).
+-export ([is_acceptable/6]).
+-include("ejabberd.hrl").
+is_acceptable(["true"]=Path, Q, Origin, Protocol, IP, Headers)->
+ ?INFO_MSG("Authorized Websocket ~p with: ~n Q = ~p~n Origin = ~p~n Protocol = ~p~n IP = ~p~n Headers = ~p~n",
+ [Path, Q, Origin, Protocol, IP, Headers]),
+ true;
+is_acceptable(["false"]=Path, Q, Origin, Protocol, IP, Headers)->
+ ?INFO_MSG("Failed Websocket ~p with: ~n Q = ~p~n Origin = ~p~n Protocol = ~p~n IP = ~p~n Headers = ~p~n",
+ [Path, Q, Origin, Protocol, IP, Headers]),
+ false. \ No newline at end of file
diff --git a/src/web/websocket_test.erl b/src/web/websocket_test.erl
new file mode 100644
index 000000000..b5491bc08
--- /dev/null
+++ b/src/web/websocket_test.erl
@@ -0,0 +1,19 @@
+-module (websocket_test).
+-export([start_link/1, loop/1]).
+
+% callback on received websockets data
+start_link(Ws) ->
+ Pid = spawn_link(?MODULE, loop, [Ws]),
+ {ok, Pid}.
+
+loop(Ws) ->
+ receive
+ {browser, Data} ->
+ Ws:send(["received '", Data, "'"]),
+ loop(Ws);
+ _Ignore ->
+ loop(Ws)
+ after 5000 ->
+ Ws:send("pushing!"),
+ loop(Ws)
+ end.
diff --git a/src/web/xmpp_json.erl b/src/web/xmpp_json.erl
new file mode 100644
index 000000000..6fae2ab08
--- /dev/null
+++ b/src/web/xmpp_json.erl
@@ -0,0 +1,362 @@
+%%%----------------------------------------------------------------------
+%%% File : xmpp_json.erl
+%%% Author : Eric Cestari <ecestari@process-one.net>
+%%% Purpose : Converts {xmlelement,Name, A, Sub} to/from JSON as per protoxep
+%%% Created : 09-20-2010
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2010 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+
+-module (xmpp_json).
+
+-export([to_json/1, from_json/1]).
+
+
+
+%%% FROM JSON TO XML
+
+from_json({struct, [{<<"stream">>, _Attr}]=Elems}) ->
+ parse_start(Elems);
+
+from_json({struct, Elems}) ->
+ {xmlstreamelement, hd(from_json2({struct, Elems}))}.
+
+from_json2({struct, Elems}) ->
+ lists:map(fun parse_json/1, Elems).
+
+parse_start([{BinName, {struct, JAttrs}}]) ->
+ Name = binary_to_list(BinName),
+ {FullName, Attrs} = lists:foldl(
+ fun({<<"xml">>, {struct, XML}}, {N, Attrs}) ->
+ XmlAttrs = parse_json_special_attrs("xml", XML),
+ {N, lists:merge(Attrs, XmlAttrs)};
+ ({<<"xmlns">>, {struct, XMLNS}}, {N, Attrs}) ->
+ XmlNsAttrs = parse_json_special_attrs("xmlns", XMLNS),
+ {N, lists:merge(Attrs, XmlNsAttrs)};
+ ({<<"$$">>, BaseNS}, {N, Attrs})->
+ {binary_to_list(BaseNS)++":"++N, Attrs};
+ ({Key, Value}, {N, Attrs})->
+ {N, [{ib2tol(Key), ib2tol(Value)}|Attrs]}
+ end, {Name, []}, JAttrs),
+ {xmlstreamstart, FullName, Attrs}.
+
+parse_json({Name, CData}) when is_binary(CData)->
+ {xmlelement, binary_to_list(Name), [], [{xmlcdata, CData}]};
+
+parse_json({Name, CDatas}) when is_list(CDatas)->
+ lists:map(fun(CData)->
+ {xmlelement, binary_to_list(Name), [], [{xmlcdata, CData}]}
+ end, CDatas);
+
+parse_json({BinName, {struct, JAttrs}}) ->
+ Name = binary_to_list(BinName),
+ {FullName, Attrs, SubEls} = lists:foldl(
+ fun({<<"$">>, Cdata}, {N, Attrs, _SubEls}) when is_binary(Cdata)->
+ {N, Attrs, [{xmlcdata, Cdata}]};
+ ({<<"$">>, {struct, Elems}}, {N, Attrs, _SubEls}) ->
+ SE = lists:map(fun parse_json/1, Elems),
+ {N, Attrs, lists:flatten(SE)}; % due to 4.2.3.3
+ ({<<"xml">>, {struct, XML}}, {N, Attrs, SubEls}) ->
+ XmlAttrs = parse_json_special_attrs("xml", XML),
+ {N, lists:merge(Attrs, XmlAttrs), SubEls};
+ ({<<"xmlns">>, {struct, XMLNS}}, {N, Attrs, SubEls}) ->
+ XmlNsAttrs = parse_json_special_attrs("xmlns", XMLNS),
+ {N, lists:merge(Attrs, XmlNsAttrs), SubEls};
+ ({Key, {struct, []}}, {N, Attrs, SubEls})->
+ {N, Attrs, [{xmlelement, ib2tol(Key), [], []}|SubEls]};
+ ({Key, Value}, {N, Attrs, SubEls})->
+ {N, [{binary_to_list(Key), ib2tol(Value)}|Attrs], SubEls}
+ end, {Name, [], []}, JAttrs),
+ {xmlelement, FullName, Attrs, SubEls}.
+
+parse_json_special_attrs(Prefix, XMLNS)->
+ lists:reverse(lists:map(
+ fun({<<"$">>, Value})->
+ {Prefix, ib2tol(Value)};
+ ({<<"@",NS/binary>>, Value})->
+ {Prefix ++ ":"++binary_to_list(NS), ib2tol(Value)}
+ end, XMLNS)).
+
+%%% FROM XML TO JSON
+to_json({xmlstreamelement, XMLElement})->
+ to_json(XMLElement);
+to_json({xmlelement, _Name, [], []})->
+ {struct, []};
+to_json({xmlelement, Name, [], [{xmlcdata, Cdata}]})->
+ {SName, JsonAttrs2} = parse_namespace(Name, []),
+ {struct, [{SName, Cdata}|JsonAttrs2]};
+to_json({xmlstreamstart, Name, Attrs})->
+ JsonAttrs = parse_attrs(Attrs),
+ {SName, Members2} = parse_namespace(Name, JsonAttrs),
+ {struct, [{SName, {struct, Members2}}]};
+to_json({xmlelement, Name, Attrs, SubEls})->
+ JsonAttrs = parse_attrs(Attrs),
+ Members = case parse_subels(SubEls) of
+ [] ->
+ JsonAttrs;
+ [Elem] ->
+ [{<<"$">>,Elem}|JsonAttrs];
+ Elems ->
+ [{<<"$">>,Elems}|JsonAttrs]
+ end,
+ {SName, Members2} = parse_namespace(Name, Members),
+ {struct, [{SName, {struct, Members2}}]}.
+
+parse_namespace(Name, AttrsList)->
+ {l2b(Name), AttrsList}.
+
+parse_subels([{xmlcdata, Cdata}])->
+ l2b(Cdata);
+parse_subels([])->
+ [];
+parse_subels(SubEls)->
+ {struct, lists:reverse(lists:foldl(
+ fun({xmlelement, SName, [], [{xmlcdata, UCdata}]}, Acc)->
+ Cdata = l2b(UCdata),
+ Name = l2b(SName),
+ case lists:keyfind(Name, 1, Acc) of
+ {Name, PrevCdata} when is_binary(PrevCdata) ->
+ Acc1 = lists:keydelete(Name, 1, Acc),
+ [{Name,[PrevCdata, Cdata]} | Acc1];
+ {Name, CDatas} when is_list(CDatas) ->
+ Acc1 = lists:keydelete(Name, 1, Acc),
+ [{Name,lists:append(CDatas, [Cdata])} | Acc1];
+ _ ->
+ [{Name, Cdata}| Acc]
+ end;
+ ({xmlelement, SName, _, _} = Elem, Acc) ->
+ E = case to_json(Elem) of %TODO There could be a better way to iterate
+ {struct, [{_, ToKeep}]} -> ToKeep;
+ {struct, []} = Empty -> Empty
+ end,
+ [{l2b(SName), E}|Acc];
+ ({xmlcdata,<<"\n">>}, Acc) ->
+ Acc
+ end,[], SubEls))}.
+
+
+parse_attrs(XmlAttrs)->
+ {Normal, XMLNS} = lists:foldl(
+ fun({"xmlns", NS}, {Attrs, XMLNS}) ->
+ {Attrs,[{<<"$">>, l2b(NS)}| XMLNS]};
+ ({"xmlns:" ++ Short, NS}, {Attrs, XMLNS})->
+ AttrName = iolist_to_binary([<<"@">>,l2b(Short)]),
+ {Attrs,[{AttrName, list_to_binary(NS)}| XMLNS]};
+ ({"xml:" ++ Short, Val}, {Attrs, XMLNS})->
+ % TODO currently tolerates only one xml:* attr per element
+ AttrName = iolist_to_binary([<<"@">>,l2b(Short)]),
+ {[{<<"xml">>,{struct, [{AttrName, l2b(Val)}]}}|Attrs], XMLNS};
+ ({K, V}, {Attrs, XMLNS})->
+ {[{l2b(K), l2b(V)}|Attrs], XMLNS}
+ end,{[], []}, XmlAttrs),
+
+ case XMLNS of
+ [{<<"$">>, NS}]->
+ [{<<"xmlns">>, NS}|Normal];
+ []->
+ Normal;
+ _ ->
+ [{<<"xmlns">>,{struct, XMLNS} }| Normal]
+ end.
+
+l2b(List) when is_list(List) -> list_to_binary(List);
+l2b(Bin) when is_binary(Bin) -> Bin.
+
+ib2tol(Bin) when is_binary(Bin) -> binary_to_list(Bin );
+ib2tol(Integer) when is_integer(Integer) -> integer_to_list(Integer);
+ib2tol(List) when is_list(List) -> List.
+
+%%
+%% Tests
+%% erlc -DTEST web/xmpp_json.erl && erl -pa web/ -run xmpp_json test -run init stop -noshell
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+% 4.2.3.1 Tag with text value
+to_text_value_test()->
+ In = {xmlstreamelement, {xmlelement, "tag", [], [{xmlcdata, <<"txt-value">>}]}},
+ Out = {struct, [{<<"tag">>, <<"txt-value">>}]},
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+
+% 4.2.3.2 Tag with recursive tags
+to_tag_with_recursive_tags_test()->
+ In = {xmlstreamelement, {xmlelement, "tag", [],
+ [{xmlelement,"tag2",[], [{xmlcdata, <<"txt-value">>}]},
+ {xmlelement,"tag3",[], [
+ {xmlelement,"tag4",[], [{xmlcdata, <<"txt2-value">>}]}]}]}},
+ Out = {struct, [{<<"tag">>,
+ {struct, [{<<"$">>,
+ {struct, [
+ {<<"tag2">>,<<"txt-value">>},
+ {<<"tag3">>,{struct, [{<<"$">>,{struct, [{<<"tag4">>,<<"txt2-value">>}]}}]}}
+ ]}
+ }]}
+ }]
+ },
+ %io:format("~n~p", [list_to_binary(mochijson2:encode(to_json(In)))]),
+ io:format("~n~p", [from_json(Out)]),
+ io:format("~n~p", [to_json(In)]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+
+% 4.2.3.3 Multiple text value tags as array
+multiple_text_value_tags_as_array_test()->
+ In = {xmlstreamelement, {xmlelement, "tag", [], [
+ {xmlelement,"tag2",[], [
+ {xmlcdata, <<"txt-value">>}]},
+ {xmlelement,"tag2",[], [
+ {xmlcdata, <<"txt-value2">>}]}]}},
+ Out = {struct, [{<<"tag">>,
+ {struct, [{<<"$">>,
+ {struct, [{<<"tag2">>,
+ [<<"txt-value">>, <<"txt-value2">>]}]}
+ }]}
+ }]
+ },
+ io:format("~p~n", [to_json(In)]),
+ io:format("~p~n", [from_json(Out)]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+
+% 4.2.3.4 Tag with attribute, no value
+tag_attr_no_value_test() ->
+ In = {xmlstreamelement, {xmlelement, "tag", [{"attr", "attr-value"}], []}},
+ Out = {struct, [{<<"tag">>, {struct, [
+ {<<"attr">>,<<"attr-value">>}
+ ]}}]},
+ io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]),
+ io:format("~p", [from_json(Out)]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+
+% 4.2.3.5 Tag with multiple attributes as array, no value
+% Not wellformed XML.
+
+% 4.2.3.6 Tags as array with unique attributes, no value
+
+
+% 4.2.3.7 Tag with namespace attribute, no value
+tag_with_namespace_no_value_test()->
+ In = {xmlstreamelement, {xmlelement, "tag", [{"xmlns:ns", "ns-value"}], []}},
+ Out = {struct, [{<<"tag">>, {struct, [
+ {<<"xmlns">>,{struct, [{<<"@ns">>, <<"ns-value">>}]}}
+ ]}}]},
+ io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+
+
+% 4.2.3.8 Tag with many attributes to namespace, no value
+two_namespaces_tag_no_value_test()->
+ In = {xmlstreamelement,{xmlelement, "tag", [{"xmlns:ns", "ns-value"},
+ {"xmlns", "root-value"}], []}},
+ Out = {struct, [{<<"tag">>, {struct, [
+ {<<"xmlns">>,{struct, [
+ {<<"$">>, <<"root-value">>},
+ {<<"@ns">>, <<"ns-value">>}]}}
+ ]}}]},
+ io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+
+% 4.2.3.9 Tag with namespace attribute, no value
+% Removed namespace handling. More complex on both sides.
+namespaced_tag_no_value_test()->
+ In = {xmlstreamelement,{xmlelement, "ns:tag", [{"attr", "attr-value"}], []}},
+ Out = {struct, [{<<"ns:tag">>, {struct, [
+ {<<"attr">>,<<"attr-value">>}
+ ]}}]},
+ io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+
+% 4.2.3.10 Tag with attribute and text value
+tag_with_attribute_and_value_test()->
+ In = {xmlstreamelement,{xmlelement, "tag", [{"attr", "attr-value"}],
+ [{xmlcdata, <<"txt-value">>}]}},
+ Out = {struct, [{<<"tag">>, {struct, [
+ {<<"$">>, <<"txt-value">>},
+ {<<"attr">>,<<"attr-value">>}
+ ]}}]},
+ %io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+
+% 4.2.3.11 Namespace tag with attribute and text value
+% Removed namespace handling. More complex on both sides
+namespaced_tag_with_value_test()->
+ In = {xmlstreamelement,{xmlelement, "ns:tag", [{"attr", "attr-value"}], [{xmlcdata, <<"txt-value">>}]}},
+ Out = {struct, [{<<"ns:tag">>, {struct, [
+ {<<"$">>,<<"txt-value">>},
+ {<<"attr">>,<<"attr-value">>}
+ ]}}]},
+ io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+
+xml_lang_attr_test()->
+ In = {xmlstreamelement,{xmlelement, "tag", [{"xml:lang", "en"}], []}},
+ Out = {struct, [{<<"tag">>, {struct, [
+ {<<"xml">>,{struct,[{<<"@lang">>,<<"en">>}]}}
+ ]}}]},
+ io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+
+xmlns_tag_with_value_test()->
+ Out = {struct,[{<<"response">>,
+ {struct,[{<<"$">>,<<"dXNlcm5hbWU9I">>},
+ {<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]}}
+ ]},
+ Out2 = {struct,[{<<"response">>,
+ {struct,[{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>},
+ {<<"$">>,<<"dXNlcm5hbWU9I">>}
+ ]}}
+ ]},
+ In = {xmlstreamelement,{xmlelement,"response",
+ [{"xmlns","urn:ietf:params:xml:ns:xmpp-sasl"}],
+ [{xmlcdata, <<"dXNlcm5hbWU9I">>}]}},
+ io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)),
+ ?assertEqual(In, from_json(Out2)).
+
+no_attr_no_value_test()->
+ In = {xmlstreamelement, {xmlelement,"failure",
+ [{"xmlns","urn:ietf:params:xml:ns:xmpp-sasl"}],
+ [{xmlelement,"not-authorized",[],[]}]}},
+ Out = {struct, [{<<"failure">>,{struct, [
+ {<<"$">>, {struct, [{<<"not-authorized">>, {struct, []}}]}},
+ {<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>}
+ ]}}]},
+ io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]),
+ io:format("~p~n", [to_json(In)]),
+ io:format("~p~n", [from_json(Out)]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+
+xmlstream_test()->
+ In = {xmlstreamstart, "stream", [{"xml:lang", "en"}]},
+ Out = {struct, [{<<"stream">>, {struct, [
+ {<<"xml">>,{struct,[{<<"@lang">>,<<"en">>}]}}
+ ]}}]},
+ io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]),
+ ?assertEqual(Out, to_json(In)),
+ ?assertEqual(In, from_json(Out)).
+-endif. \ No newline at end of file
diff --git a/src/xml.erl b/src/xml.erl
index 6af94f6a0..6a0c61aae 100644
--- a/src/xml.erl
+++ b/src/xml.erl
@@ -31,6 +31,7 @@
element_to_binary/1,
crypt/1, make_text_node/1,
remove_cdata/1,
+ remove_subtags/3,
get_cdata/1, get_tag_cdata/1,
get_attr/2, get_attr_s/2,
get_tag_attr/2, get_tag_attr_s/2,
@@ -187,6 +188,31 @@ remove_cdata_p(_) -> false.
remove_cdata(L) -> [E || E <- L, remove_cdata_p(E)].
+%% TODO: Make more generic.
+%% For now only support all parameters:
+%% xml:remove_subtags({xmlelement,"message", [{"id","81be72"}],[{xmlelement,"on-sender-server",[{"xmlns","urn:xmpp:receipts"},{"server","text-one.com"}], []}]}, "on-sender-server", {"xmlns","urn:xmpp:receipts"}).
+remove_subtags({xmlelement, TagName, TagAttrs, Els}, Name, Attr) ->
+ {xmlelement, TagName, TagAttrs, remove_subtags1(Els, [], Name, Attr)}.
+
+remove_subtags1([], NewEls, _Name, _Attr) ->
+ lists:reverse(NewEls);
+remove_subtags1([El | Els], NewEls, Name, {AttrName, AttrValue} = Attr) ->
+ case El of
+ {xmlelement, Name, Attrs, _} ->
+ case get_attr(AttrName, Attrs) of
+ false ->
+ remove_subtags1(Els, [El|NewEls], Name, Attr);
+ {value, AttrValue} ->
+ remove_subtags1(Els, NewEls, Name, Attr);
+ _ ->
+ remove_subtags1(Els, [El|NewEls], Name, Attr)
+ end;
+ _ ->
+ remove_subtags1(Els, [El|NewEls], Name, Attr)
+ end.
+
+
+
get_cdata(L) ->
binary_to_list(list_to_binary(get_cdata(L, ""))).
diff --git a/src/xml_stream.erl b/src/xml_stream.erl
index 2e0547ead..83dc5764c 100644
--- a/src/xml_stream.erl
+++ b/src/xml_stream.erl
@@ -31,6 +31,7 @@
new/2,
parse/2,
close/1,
+ change_callback_pid/2,
parse_element/1]).
-define(XML_START, 0).
@@ -75,6 +76,9 @@ process_data(CallbackPid, Stack, Data) ->
{?XML_CDATA, CData} ->
case Stack of
[El] ->
+ catch gen_fsm:send_all_state_event(
+ CallbackPid,
+ {xmlstreamcdata, CData}),
[El];
%% Merge CDATA nodes if they are contiguous
%% This does not change the semantic: the split in
@@ -88,7 +92,8 @@ process_data(CallbackPid, Stack, Data) ->
[{xmlelement, Name, Attrs, Els} | Tail] ->
[{xmlelement, Name, Attrs, [{xmlcdata, CData} | Els]} |
Tail];
- [] -> []
+ [] ->
+ []
end;
{?XML_ERROR, Err} ->
catch gen_fsm:send_event(CallbackPid, {xmlstreamerror, Err})
@@ -106,6 +111,8 @@ new(CallbackPid, MaxSize) ->
size = 0,
maxsize = MaxSize}.
+change_callback_pid(State, CallbackPid) ->
+ State#xml_stream_state{callback_pid = CallbackPid}.
parse(#xml_stream_state{callback_pid = CallbackPid,
port = Port,
diff --git a/src/xmlrpc.erl b/src/xmlrpc.erl
new file mode 100644
index 000000000..102b75a94
--- /dev/null
+++ b/src/xmlrpc.erl
@@ -0,0 +1,187 @@
+%% Copyright (C) 2003 Joakim Grebenö <jocke@gleipnir.com>.
+%% All rights reserved.
+%%
+%% Redistribution and use in source and binary forms, with or without
+%% modification, are permitted provided that the following conditions
+%% are met:
+%%
+%% 1. Redistributions of source code must retain the above copyright
+%% notice, this list of conditions and the following disclaimer.
+%% 2. 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.
+%%
+%% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
+
+-module(xmlrpc).
+-author('jocke@gleipnir.com').
+-export([call/3, call/4, call/5, call/6]).
+-export([start_link/1, start_link/5, start_link/6, stop/1]).
+
+-include("log.hrl").
+
+-record(header, {
+ %% int()
+ content_length,
+ %% close | undefined
+ connection
+ }).
+
+%% Exported: call/{3,4,5,6}
+
+call(Host, Port, URI, Payload) -> call(Host, Port, URI, Payload, false, 60000).
+
+call(Host, Port, URI, Payload, KeepAlive, Timeout) ->
+ case gen_tcp:connect(Host, Port, [{active, false}]) of
+ {ok, Socket} -> call(Socket, URI, Payload, KeepAlive, Timeout);
+ {error, Reason} when KeepAlive == false -> {error, Reason};
+ {error, Reason} -> {error, undefined, Reason}
+ end.
+
+call(Socket, URI, Payload) -> call(Socket, URI, Payload, false, 60000).
+
+call(Socket, URI, Payload, KeepAlive, Timeout) ->
+ ?DEBUG_LOG({decoded_call, Payload}),
+ case xmlrpc_encode:payload(Payload) of
+ {ok, EncodedPayload} ->
+ ?DEBUG_LOG({encoded_call, EncodedPayload}),
+ case send(Socket, URI, KeepAlive, EncodedPayload) of
+ ok ->
+ case parse_response(Socket, Timeout) of
+ {ok, Header} ->
+ handle_payload(Socket, KeepAlive, Timeout, Header);
+ {error, Reason} when KeepAlive == false ->
+ gen_tcp:close(Socket),
+ {error, Reason};
+ {error, Reason} -> {error, Socket, Reason}
+ end;
+ {error, Reason} when KeepAlive == false ->
+ gen_tcp:close(Socket),
+ {error, Reason};
+ {error, Reason} -> {error, Socket, Reason}
+ end;
+ {error, Reason} when KeepAlive == false ->
+ gen_tcp:close(Socket),
+ {error, Reason};
+ {error, Reason} -> {error, Socket, Reason}
+ end.
+
+send(Socket, URI, false, Payload) ->
+ send(Socket, URI, "Connection: close\r\n", Payload);
+send(Socket, URI, true, Payload) -> send(Socket, URI, "", Payload);
+send(Socket, URI, Header, Payload) ->
+ Request =
+ ["POST ", URI, " HTTP/1.1\r\n",
+ "Content-Length: ", integer_to_list(lists:flatlength(Payload)),
+ "\r\n",
+ "User-Agent: Erlang XML-RPC Client 1.13\r\n",
+ "Content-Type: text/xml\r\n",
+ Header, "\r\n",
+ Payload],
+ gen_tcp:send(Socket, Request).
+
+parse_response(Socket, Timeout) ->
+ inet:setopts(Socket, [{packet, line}]),
+ case gen_tcp:recv(Socket, 0, Timeout) of
+ {ok, "HTTP/1.1 200 OK\r\n"} -> parse_header(Socket, Timeout);
+ {ok, StatusLine} -> {error, StatusLine};
+ {error, Reason} -> {error, Reason}
+ end.
+
+parse_header(Socket, Timeout) -> parse_header(Socket, Timeout, #header{}).
+
+parse_header(Socket, Timeout, Header) ->
+ case gen_tcp:recv(Socket, 0, Timeout) of
+ {ok, "\r\n"} when Header#header.content_length == undefined ->
+ {error, missing_content_length};
+ {ok, "\r\n"} -> {ok, Header};
+ {ok, HeaderField} ->
+ case string:tokens(HeaderField, " \r\n") of
+ ["Content-Length:", ContentLength] ->
+ case catch list_to_integer(ContentLength) of
+ badarg ->
+ {error, {invalid_content_length, ContentLength}};
+ Value ->
+ parse_header(Socket, Timeout,
+ Header#header{content_length =
+ Value})
+ end;
+ ["Connection:", "close"] ->
+ parse_header(Socket, Timeout,
+ Header#header{connection = close});
+ _ ->
+ parse_header(Socket, Timeout, Header)
+ end;
+ {error, Reason} -> {error, Reason}
+ end.
+
+handle_payload(Socket, KeepAlive, Timeout, Header) ->
+ case get_payload(Socket, Timeout, Header#header.content_length) of
+ {ok, Payload} ->
+ ?DEBUG_LOG({encoded_response, Payload}),
+ case xmlrpc_decode:payload(Payload) of
+ {ok, DecodedPayload} when KeepAlive == false ->
+ ?DEBUG_LOG({decoded_response, DecodedPayload}),
+ gen_tcp:close(Socket),
+ {ok, DecodedPayload};
+ {ok, DecodedPayload} when KeepAlive == true,
+ Header#header.connection == close ->
+ ?DEBUG_LOG({decoded_response, DecodedPayload}),
+ gen_tcp:close(Socket),
+ {ok, Socket, DecodedPayload};
+ {ok, DecodedPayload} ->
+ ?DEBUG_LOG({decoded_response, DecodedPayload}),
+ {ok, Socket, DecodedPayload};
+ {error, Reason} when KeepAlive == false ->
+ gen_tcp:close(Socket),
+ {error, Reason};
+ {error, Reason} when KeepAlive == true,
+ Header#header.connection == close ->
+ gen_tcp:close(Socket),
+ {error, Socket, Reason};
+ {error, Reason} ->
+ {error, Socket, Reason}
+ end;
+ {error, Reason} when KeepAlive == false ->
+ gen_tcp:close(Socket),
+ {error, Reason};
+ {error, Reason} when KeepAlive == true,
+ Header#header.connection == close ->
+ gen_tcp:close(Socket),
+ {error, Socket, Reason};
+ {error, Reason} -> {error, Socket, Reason}
+ end.
+
+get_payload(Socket, Timeout, ContentLength) ->
+ inet:setopts(Socket, [{packet, raw}]),
+ gen_tcp:recv(Socket, ContentLength, Timeout).
+
+%% Exported: start_link/{1,5,6}
+
+start_link(Handler) -> start_link(4567, 1000, 60000, Handler, undefined).
+
+start_link(Port, MaxSessions, Timeout, Handler, State) ->
+ start_link(all, Port, MaxSessions, Timeout, Handler, State).
+
+start_link(IP, Port, MaxSessions, Timeout, Handler, State) ->
+ OptionList = [{active, false}, {reuseaddr, true}] ++ ip(IP),
+ SessionHandler = {xmlrpc_http, handler, [Timeout, Handler, State]},
+ tcp_serv:start_link([Port, MaxSessions, OptionList, SessionHandler]).
+
+ip(all) -> [];
+ip(IP) when tuple(IP) -> [{ip, IP}].
+
+%% Exported: stop/1
+
+stop(Pid) -> tcp_serv:stop(Pid).
diff --git a/src/xmlrpc_decode.erl b/src/xmlrpc_decode.erl
new file mode 100644
index 000000000..af0d72c68
--- /dev/null
+++ b/src/xmlrpc_decode.erl
@@ -0,0 +1,218 @@
+%% Copyright (C) 2003 Joakim Grebenö <jocke@gleipnir.com>.
+%% All rights reserved.
+%%
+%% Redistribution and use in source and binary forms, with or without
+%% modification, are permitted provided that the following conditions
+%% are met:
+%%
+%% 1. Redistributions of source code must retain the above copyright
+%% notice, this list of conditions and the following disclaimer.
+%% 2. 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.
+%%
+%% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
+
+-module(xmlrpc_decode).
+-author('jocke@gleipnir.com').
+-export([payload/1]).
+
+-include("log.hrl").
+-include_lib("xmerl/include/xmerl.hrl").
+
+payload(Payload) ->
+ ?DEBUG_LOG({scanning_payload, Payload}),
+ case xmerl_scan:string(Payload) of
+ {error, Reason} ->
+ ?DEBUG_LOG({error_scanning, Payload, Reason}),
+ {error, Reason};
+ {E, _} ->
+ ?DEBUG_LOG({decoding_element, E}),
+ case catch decode_element(E) of
+ {'EXIT', Reason} ->
+ ?DEBUG_LOG({error_deconding, E, Reason}),
+ exit(Reason);
+ Result ->
+ ?DEBUG_LOG({result_deconding, E, Result}),
+ Result
+ end
+ end.
+
+decode_element(#xmlElement{name = methodCall} = MethodCall)
+ when record(MethodCall, xmlElement) ->
+ {MethodName, Rest} =
+ match_element([methodName], MethodCall#xmlElement.content),
+ TextValue = get_text_value(MethodName#xmlElement.content),
+ case match_element(normal, [params], Rest) of
+ {error, {missing_element, _}} ->
+ {ok, {call, list_to_atom(TextValue), []}};
+ {Params, _} ->
+ DecodedParams = decode_params(Params#xmlElement.content),
+ {ok, {call, list_to_atom(TextValue), DecodedParams}}
+ end;
+decode_element(#xmlElement{name = methodResponse} = MethodResponse)
+ when record(MethodResponse, xmlElement) ->
+ case match_element([fault, params], MethodResponse#xmlElement.content) of
+ {Fault, _} when Fault#xmlElement.name == fault ->
+ {Value, _} = match_element([value], Fault#xmlElement.content),
+ case decode(Value#xmlElement.content) of
+ {struct, [{faultCode, Code},
+ {faultString, String}]} when integer(Code) ->
+ case xmlrpc_util:is_string(String) of
+ yes -> {ok, {response, {fault, Code, String}}};
+ no -> {error, {bad_string, String}}
+ end;
+ _ ->
+ {error, {bad_element, MethodResponse}}
+ end;
+ {Params, _} ->
+ case decode_params(Params#xmlElement.content) of
+ [DecodedParam] -> {ok, {response, [DecodedParam]}};
+ DecodedParams -> {error, {to_many_params, DecodedParams}}
+ end
+ end;
+decode_element(E) -> {error, {bad_element, E}}.
+
+match_element(NameList, Content) -> match_element(throw, NameList, Content).
+
+match_element(Type, NameList, []) ->
+ return(Type, {error, {missing_element, NameList}});
+match_element(Type, NameList, [E|Rest]) when record(E, xmlElement) ->
+ case lists:member(E#xmlElement.name, NameList) of
+ true -> {E, Rest};
+ false -> return(Type, {error, {unexpected_element, E#xmlElement.name}})
+ end;
+match_element(Type, NameList, [T|Rest]) when record(T, xmlText) ->
+ case only_whitespace(T#xmlText.value) of
+ yes -> match_element(Type, NameList, Rest);
+ no ->
+ return(Type, {error, {unexpected_text, T#xmlText.value, NameList}})
+ end.
+
+return(throw, Result) -> throw(Result);
+return(normal, Result) -> Result.
+
+only_whitespace([]) -> yes;
+only_whitespace([$ |Rest]) -> only_whitespace(Rest);
+only_whitespace([$\n|Rest]) -> only_whitespace(Rest);
+only_whitespace([$\t|Rest]) -> only_whitespace(Rest);
+only_whitespace(_) -> no.
+
+get_text_value([]) -> [];
+get_text_value([T|Rest]) when record(T, xmlText) ->
+ T#xmlText.value++get_text_value(Rest);
+get_text_value(_) -> throw({error, missing_text}).
+
+decode_params([]) -> [];
+decode_params(Content) ->
+ case match_element(normal, [param], Content) of
+ {error, {missing_element, _}} -> [];
+ {Param, Rest} ->
+ {Value, _} = match_element([value], Param#xmlElement.content),
+ [decode(Value#xmlElement.content)|decode_params(Rest)]
+ end.
+
+decode(Content) when list(Content) ->
+ case get_value(Content) of
+ {text_value, TextValue} -> TextValue;
+ E -> decode(E)
+ end;
+decode(String) when record(String, xmlText) -> String#xmlText.value;
+decode(Struct) when Struct#xmlElement.name == struct ->
+ {struct, decode_members(Struct#xmlElement.content)};
+decode(Array) when Array#xmlElement.name == array ->
+ {Data, _} = match_element([data], Array#xmlElement.content),
+ {array, decode_values(Data#xmlElement.content)};
+decode(Int) when Int#xmlElement.name == int; Int#xmlElement.name == i4 ->
+ TextValue = get_text_value(Int#xmlElement.content),
+ make_integer(TextValue);
+decode(Boolean) when Boolean#xmlElement.name == boolean ->
+ case get_text_value(Boolean#xmlElement.content) of
+ "1" -> true;
+ "0" -> false;
+ TextValue -> throw({error, {invalid_boolean, TextValue}})
+ end;
+decode(String) when String#xmlElement.name == string ->
+ get_text_value(String#xmlElement.content);
+decode(Double) when Double#xmlElement.name == double ->
+ TextValue = get_text_value(Double#xmlElement.content),
+ make_double(TextValue);
+decode(Date) when Date#xmlElement.name == 'dateTime.iso8601' ->
+ TextValue = get_text_value(Date#xmlElement.content),
+ {date, ensure_iso8601_date(TextValue)};
+decode(Base64) when Base64#xmlElement.name == base64 ->
+ TextValue = get_text_value(Base64#xmlElement.content),
+ {base64, ensure_base64(TextValue)};
+decode(Value) -> throw({error, {bad_value, Value}}).
+
+get_value(Content) ->
+ case any_element(Content) of
+ false -> {text_value, get_text_value(Content)};
+ true -> get_element(Content)
+ end.
+
+any_element([]) -> false;
+any_element([E|_]) when record(E, xmlElement) -> true;
+any_element([_|Rest]) -> any_element(Rest).
+
+get_element([]) -> throw({error, missing_element});
+get_element([E|_]) when record(E, xmlElement) -> E;
+get_element([T|Rest]) when record(T, xmlText) ->
+ case only_whitespace(T#xmlText.value) of
+ yes -> get_element(Rest);
+ no -> throw({error, {unexpected_text, T#xmlText.value}})
+ end.
+
+decode_members(Content) ->
+ case match_element(normal, [member], Content) of
+ {error, {missing_element, _}} -> [];
+ {Member, Rest} ->
+ {Name, Rest2} = match_element([name], Member#xmlElement.content),
+ TextValue = get_text_value(Name#xmlElement.content),
+ {Value, _} = match_element([value], Rest2),
+ [{list_to_atom(TextValue),
+ decode(Value#xmlElement.content)}|decode_members(Rest)]
+ end.
+
+decode_values([]) -> [];
+decode_values(Content) ->
+ case match_element(normal, [value], Content) of
+ {error, {missing_element, _}} -> [];
+ {Value, Rest} ->
+ [decode(Value#xmlElement.content)|decode_values(Rest)]
+ end.
+
+make_integer(Integer) ->
+ case catch list_to_integer(Integer) of
+ {'EXIT', Reason} -> throw({error, {not_integer, Integer}});
+ Value -> Value
+ end.
+
+make_double(Double) ->
+ case catch list_to_float(Double) of
+ {'EXIT', _} -> throw({error, {not_double, Double}});
+ Value -> Value
+ end.
+
+ensure_iso8601_date(Date) ->
+ case xmlrpc_util:is_iso8601_date(Date) of
+ no -> throw({error, {not_iso8601_date, Date}});
+ yes -> Date
+ end.
+
+ensure_base64(Base64) ->
+ case xmlrpc_util:is_base64(Base64) of
+ no -> throw({error, {not_base64, Base64}});
+ yes -> Base64
+ end.
diff --git a/src/xmlrpc_encode.erl b/src/xmlrpc_encode.erl
new file mode 100644
index 000000000..be2d3b526
--- /dev/null
+++ b/src/xmlrpc_encode.erl
@@ -0,0 +1,145 @@
+%% Copyright (C) 2003 Joakim Grebenö <jocke@gleipnir.com>.
+%% All rights reserved.
+%%
+%% Redistribution and use in source and binary forms, with or without
+%% modification, are permitted provided that the following conditions
+%% are met:
+%%
+%% 1. Redistributions of source code must retain the above copyright
+%% notice, this list of conditions and the following disclaimer.
+%% 2. 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.
+%%
+%% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
+
+-module(xmlrpc_encode).
+-author('jocke@gleipnir.com').
+-export([payload/1]).
+
+%% Exported: payload/1
+
+payload({call, Name, Params}) when atom(Name), list(Params) ->
+ case encode_params(Params) of
+ {error, Reason} -> {error, Reason};
+ EncodedParams ->
+ EncodedPayload =
+ ["<?xml version=\"1.0\"?><methodCall><methodName>",
+ atom_to_list(Name), "</methodName>", EncodedParams,
+ "</methodCall>"],
+ {ok, EncodedPayload}
+ end;
+payload({response, {fault, Code, String}}) when integer(Code) ->
+ case xmlrpc_util:is_string(String) of
+ yes ->
+ EncodedPayload =
+ ["<?xml version=\"1.0\"?><methodResponse><fault>"
+ "<value><struct><member><name>faultCode</name><value><int>",
+ integer_to_list(Code), "</int></value></member><member><name>"
+ "faultString</name><value><string>", escape_string(String),
+ "</string></value></member></struct></value></fault>",
+ "</methodResponse>"],
+ {ok, EncodedPayload};
+ no -> {error, {bad_string, String}}
+ end;
+payload({response, []} = Payload) ->
+ {ok, ["<?xml version=\"1.0\"?><methodResponse></methodResponse>"]};
+payload({response, [Param]} = Payload) ->
+ case encode_params([Param]) of
+ {error, Reason} -> {error, Reason};
+ EncodedParam ->
+ {ok, ["<?xml version=\"1.0\"?><methodResponse>", EncodedParam,
+ "</methodResponse>"]}
+ end;
+payload(Payload) -> {error, {bad_payload, Payload}}.
+
+encode_params(Params) -> encode_params(Params, []).
+
+encode_params([], []) -> [];
+encode_params([], Acc) -> ["<params>", Acc, "</params>"];
+encode_params([Param|Rest], Acc) ->
+ case encode(Param) of
+ {error, Reason} -> {error, Reason};
+ EncodedParam ->
+ NewAcc = Acc++["<param><value>", EncodedParam, "</value></param>"],
+ encode_params(Rest, NewAcc)
+ end.
+
+encode({struct, Struct}) ->
+ case encode_members(Struct) of
+ {error, Reason} -> {error, Reason};
+ Members -> ["<struct>", Members, "</struct>"]
+ end;
+encode({array, Array}) when list(Array) ->
+ case encode_values(Array)of
+ {error, Reason} -> {error, Reason};
+ Values -> ["<array><data>", Values, "</data></array>"]
+ end;
+encode(Integer) when integer(Integer) ->
+ ["<int>", integer_to_list(Integer), "</int>"];
+encode(true) -> "<boolean>1</boolean>"; % duh!
+encode(false) -> "<boolean>0</boolean>"; % duh!
+encode(Double) when float(Double) ->
+ ["<double>", io_lib:format("~p", [Double]), "</double>"];
+encode({date, Date}) ->
+ case xmlrpc_util:is_iso8601_date(Date) of
+ yes -> ["<dateTime.iso8601>", Date, "</dateTime.iso8601>"];
+ no -> {error, {bad_date, Date}}
+ end;
+encode({base64, Base64}) ->
+ case xmlrpc_util:is_base64(Base64) of
+ yes -> ["<base64>", Base64, "</base64>"];
+ no -> {error, {bad_base64, Base64}}
+ end;
+encode(Value) ->
+ case xmlrpc_util:is_string(Value) of
+ yes -> escape_string(Value);
+ no -> {error, {bad_value, Value}}
+ end.
+
+escape_string([]) -> [];
+escape_string([$<|Rest]) -> ["&lt;", escape_string(Rest)];
+escape_string([$>|Rest]) -> ["&gt;", escape_string(Rest)];
+escape_string([$&|Rest]) -> ["&amp;", escape_string(Rest)];
+escape_string([C|Rest]) -> [C|escape_string(Rest)].
+
+encode_members(Struct) -> encode_members(Struct, []).
+
+encode_members([], Acc) -> Acc;
+encode_members([{Name, Value}|Rest], Acc) when atom(Name) ->
+ case encode(Value) of
+ {error, Reason} -> {error, Reason};
+ EncodedValue ->
+ NewAcc =
+ Acc++["<member><name>", atom_to_list(Name), "</name><value>",
+ EncodedValue, "</value></member>"],
+ encode_members(Rest, NewAcc)
+ end;
+encode_members([{Name, Value}|Rest], Acc) -> {error, {invalid_name, Name}};
+encode_members(UnknownMember, Acc) ->
+ {error, {unknown_member, UnknownMember}}.
+
+encode_values(Array) -> encode_values(Array, []).
+
+encode_values([], Acc) -> Acc;
+encode_values([Value|Rest], Acc) ->
+ case encode(Value) of
+ {error, Reason} -> {error, Reason};
+ EncodedValue ->
+ NewAcc = Acc++["<value>", EncodedValue, "</value>"],
+ encode_values(Rest, NewAcc)
+ end;
+encode_values([{Name, Value}|Rest], Acc) -> {error, {invalid_name, Name}};
+encode_values(UnknownMember, Acc) ->
+ {error, {unknown_member, UnknownMember}}.
diff --git a/src/xmlrpc_http.erl b/src/xmlrpc_http.erl
new file mode 100644
index 000000000..b2970b3e0
--- /dev/null
+++ b/src/xmlrpc_http.erl
@@ -0,0 +1,210 @@
+%% Copyright (C) 2003 Joakim Grebenö <jocke@gleipnir.com>.
+%% All rights reserved.
+%%
+%% Redistribution and use in source and binary forms, with or without
+%% modification, are permitted provided that the following conditions
+%% are met:
+%%
+%% 1. Redistributions of source code must retain the above copyright
+%% notice, this list of conditions and the following disclaimer.
+%% 2. 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.
+%%
+%% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
+
+-module(xmlrpc_http).
+-author('jocke@gleipnir.com').
+-export([handler/4]).
+
+-include("log.hrl").
+
+-record(header, {
+ %% int()
+ content_length,
+ %% string()
+ content_type,
+ %% string()
+ user_agent,
+ %% close | undefined
+ connection
+ }).
+
+%% Exported: handler/3
+
+handler(Socket, Timeout, Handler, State) ->
+ case parse_request(Socket, Timeout) of
+ {ok, Header} ->
+ ?DEBUG_LOG({header, Header}),
+ handle_payload(Socket, Timeout, Handler, State, Header);
+ {status, StatusCode} ->
+ send(Socket, StatusCode),
+ handler(Socket, Timeout, Handler, State);
+ {error, Reason} -> {error, Reason}
+ end.
+
+parse_request(Socket, Timeout) ->
+ inet:setopts(Socket, [{packet, line}]),
+ case gen_tcp:recv(Socket, 0, Timeout) of
+ {ok, RequestLine} ->
+ case string:tokens(RequestLine, " \r\n") of
+ ["POST", _, "HTTP/1.0"] ->
+ ?DEBUG_LOG({http_version, "1.0"}),
+ parse_header(Socket, Timeout, #header{connection = close});
+ ["POST", _, "HTTP/1.1"] ->
+ ?DEBUG_LOG({http_version, "1.1"}),
+ parse_header(Socket, Timeout);
+ [Method, _, "HTTP/1.1"] -> {status, 501};
+ ["POST", _, HTTPVersion] -> {status, 505};
+ _ -> {status, 400}
+ end;
+ {error, Reason} -> {error, Reason}
+ end.
+
+parse_header(Socket, Timeout) -> parse_header(Socket, Timeout, #header{}).
+
+parse_header(Socket, Timeout, Header) ->
+ case gen_tcp:recv(Socket, 0, Timeout) of
+ {ok, "\r\n"} when Header#header.content_length == undefined ->
+ {status, 411};
+ {ok, "\r\n"} when Header#header.content_type == undefined ->
+ {status, 400};
+ {ok, "\r\n"} when Header#header.user_agent == undefined ->
+ {status, 400};
+ {ok, "\r\n"} -> {ok, Header};
+ {ok, HeaderField} ->
+ case split_header_field(HeaderField) of
+ {[$C,$o,$n,$t,$e,$n,$t,$-,_,$e,$n,$g,$t,$h,$:],
+ ContentLength} ->
+ case catch list_to_integer(ContentLength) of
+ N ->
+ parse_header(Socket, Timeout,
+ Header#header{content_length = N});
+ _ -> {status, 400}
+ end;
+ {"Content-Type:", "text/xml"} ->
+ parse_header(Socket, Timeout,
+ Header#header{content_type = "text/xml"});
+ {"Content-Type:", "text/xml; charset=utf-8"} ->
+ parse_header(Socket, Timeout,
+ Header#header{content_type = "text/xml; charset=utf-8"});
+ {"Content-Type:", ContentType} -> {status, 415};
+ {"User-Agent:", UserAgent} ->
+ parse_header(Socket, Timeout,
+ Header#header{user_agent = UserAgent});
+ {"Connection:", "close"} ->
+ parse_header(Socket, Timeout,
+ Header#header{connection = close});
+ {"Connection:", [_,$e,$e,$p,$-,_,$l,$i,$v,$e]} ->
+ parse_header(Socket, Timeout,
+ Header#header{connection = undefined});
+ _ ->
+ ?DEBUG_LOG({skipped_header, HeaderField}),
+ parse_header(Socket, Timeout, Header)
+ end;
+ {error, Reason} -> {error, Reason}
+ end.
+
+split_header_field(HeaderField) -> split_header_field(HeaderField, []).
+
+split_header_field([], Name) -> {Name, ""};
+split_header_field([$ |Rest], Name) -> {lists:reverse(Name), Rest -- "\r\n"};
+split_header_field([C|Rest], Name) -> split_header_field(Rest, [C|Name]).
+
+handle_payload(Socket, Timeout, Handler, State,
+ #header{connection = Connection} = Header) ->
+ case get_payload(Socket, Timeout, Header#header.content_length) of
+ {ok, Payload} ->
+ ?DEBUG_LOG({encoded_call, Payload}),
+ case xmlrpc_decode:payload(Payload) of
+ {ok, DecodedPayload} ->
+ ?DEBUG_LOG({decoded_call, DecodedPayload}),
+ eval_payload(Socket, Timeout, Handler, State, Connection,
+ DecodedPayload);
+ {error, Reason} when Connection == close ->
+ ?ERROR_LOG({xmlrpc_decode, payload, Payload, Reason}),
+ send(Socket, 400);
+ {error, Reason} ->
+ ?ERROR_LOG({xmlrpc_decode, payload, Payload, Reason}),
+ send(Socket, 400),
+ handler(Socket, Timeout, Handler, State)
+ end;
+ {error, Reason} -> {error, Reason}
+ end.
+
+get_payload(Socket, Timeout, ContentLength) ->
+ inet:setopts(Socket, [{packet, raw}]),
+ gen_tcp:recv(Socket, ContentLength, Timeout).
+
+eval_payload(Socket, Timeout, {M, F} = Handler, State, Connection, Payload) ->
+ case catch M:F(State, Payload) of
+ {'EXIT', Reason} when Connection == close ->
+ ?ERROR_LOG({M, F, {'EXIT', Reason}}),
+ send(Socket, 500, "Connection: close\r\n");
+ {'EXIT', Reason} ->
+ ?ERROR_LOG({M, F, {'EXIT', Reason}}),
+ send(Socket, 500),
+ handler(Socket, Timeout, Handler, State);
+ {error, Reason} when Connection == close ->
+ ?ERROR_LOG({M, F, Reason}),
+ send(Socket, 500, "Connection: close\r\n");
+ {error, Reason} ->
+ ?ERROR_LOG({M, F, Reason}),
+ send(Socket, 500),
+ handler(Socket, Timeout, Handler, State);
+ {false, ResponsePayload} ->
+ encode_send(Socket, 200, "Connection: close\r\n", ResponsePayload);
+ {true, NewTimeout, NewState, ResponsePayload} when
+ Connection == close ->
+ encode_send(Socket, 200, "Connection: close\r\n", ResponsePayload);
+ {true, NewTimeout, NewState, ResponsePayload} ->
+ encode_send(Socket, 200, "", ResponsePayload),
+ handler(Socket, NewTimeout, Handler, NewState)
+ end.
+
+encode_send(Socket, StatusCode, ExtraHeader, Payload) ->
+ ?DEBUG_LOG({decoded_response, Payload}),
+ case xmlrpc_encode:payload(Payload) of
+ {ok, EncodedPayload} ->
+ ?DEBUG_LOG({encoded_response, lists:flatten(EncodedPayload)}),
+ send(Socket, StatusCode, ExtraHeader, EncodedPayload);
+ {error, Reason} ->
+ ?ERROR_LOG({xmlrpc_encode, payload, Payload, Reason}),
+ send(Socket, 500)
+ end.
+
+send(Socket, StatusCode) -> send(Socket, StatusCode, "", "").
+
+send(Socket, StatusCode, ExtraHeader) ->
+ send(Socket, StatusCode, ExtraHeader, "").
+
+send(Socket, StatusCode, ExtraHeader, Payload) ->
+ Response =
+ ["HTTP/1.1 ", integer_to_list(StatusCode), " ",
+ reason_phrase(StatusCode), "\r\n",
+ "Content-Length: ", integer_to_list(lists:flatlength(Payload)),
+ "\r\n",
+ "Server: Erlang/1.13\r\n",
+ "Content-Type: text/xml\r\n",
+ ExtraHeader, "\r\n",
+ Payload],
+ gen_tcp:send(Socket, Response).
+
+reason_phrase(200) -> "OK";
+reason_phrase(400) -> "Bad Request";
+reason_phrase(411) -> "Length required";
+reason_phrase(415) -> "Unsupported Media Type";
+reason_phrase(500) -> "Internal Server Error";
+reason_phrase(501) -> "Not Implemented";
+reason_phrase(505) -> "HTTP Version not supported".
diff --git a/src/xmlrpc_util.erl b/src/xmlrpc_util.erl
new file mode 100644
index 000000000..d46c5c07b
--- /dev/null
+++ b/src/xmlrpc_util.erl
@@ -0,0 +1,37 @@
+%% Copyright (C) 2003 Joakim Grebenö <jocke@gleipnir.com>.
+%% All rights reserved.
+%%
+%% Redistribution and use in source and binary forms, with or without
+%% modification, are permitted provided that the following conditions
+%% are met:
+%%
+%% 1. Redistributions of source code must retain the above copyright
+%% notice, this list of conditions and the following disclaimer.
+%% 2. 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.
+%%
+%% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
+
+-module(xmlrpc_util).
+-author('jocke@gleipnir.com').
+-export([is_string/1, is_iso8601_date/1, is_base64/1]).
+
+is_string([C|Rest]) when C >= 0, C =< 255 -> is_string(Rest);
+is_string([]) -> yes;
+is_string(_) -> no.
+
+is_iso8601_date(_) -> yes. % FIXME
+
+is_base64(_) -> yes. % FIXME