diff options
Diffstat (limited to 'src')
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]) -> ["<", escape_string(Rest)]; +escape_string([$>|Rest]) -> [">", escape_string(Rest)]; +escape_string([$&|Rest]) -> ["&", 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 |
