diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..11269d0ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +Makefile +archives +build/ +*.dSYM/ +.gitignore +*.so +contrib/extract_translations/extract_translations.beam +doc/contributed_modules.tex +doc/features.aux +doc/features.log +doc/features.out +doc/features.pdf +doc/guide.aux +doc/guide.idx +doc/guide.ilg +doc/guide.ind +doc/guide.log +doc/guide.out +doc/guide.pdf +doc/guide.toc +doc/version.aux +src/*.beam +src/*.so +src/eldap/eldap_filter_yecc.erl +src/XmppAddr.asn1db +src/XmppAddr.erl +src/XmppAddr.hrl +src/config.log +src/config.status +src/eldap/ELDAPv3.asn1db +src/eldap/ELDAPv3.beam +src/eldap/ELDAPv3.erl +src/eldap/ELDAPv3.hrl +src/epam +src/ejabberdctl.example +src/ejabberd.init diff --git a/contrib/ejabberd-modules.repo b/contrib/ejabberd-modules.repo deleted file mode 100644 index dfd4a1d17..000000000 --- a/contrib/ejabberd-modules.repo +++ /dev/null @@ -1,5 +0,0 @@ -% List of ejabberd-modules to add for ejabberd packaging (source archive and installer) -% -% HTTP-binding: -%https://svn.process-one.net/ejabberd-modules/http_bind/trunk -%https://svn.process-one.net/ejabberd-modules/mod_http_fileserver/trunk diff --git a/doc/guide.html b/doc/guide.html index 57873a873..50cdca74a 100644 --- a/doc/guide.html +++ b/doc/guide.html @@ -1944,8 +1944,8 @@ Mnesia as backend. (see 3.2) as backend.
  • ‘_ldap’, this means that the module needs an LDAP server as backend.
  • If you want to, -it is possible to use a relational database to store pieces of -information. You can do this by changing the module name to a name with an +it is possible to use a relational database to store the tables created by some ejabberd modules. +You can do this by changing the module name to a name with an _odbc suffix in ejabberd config file. You can use a relational database for the following data:

    You can find more contributed modules on the ejabberd website. Please remember that these contributions might not work or diff --git a/doc/guide.tex b/doc/guide.tex index 044fab829..8bcd9b25f 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -386,6 +386,9 @@ Some options that you may be interested in modifying: \titem{--enable-nif} Replaces some critical Erlang functions with equivalents written in C to improve performance. This feature requires Erlang/OTP R13B04 or higher. + + \titem{--enable-flash-hack} + Enable support for non-standard XML socket clients of Adobe Flash 8 and lower. \end{description} \makesubsection{install}{Install} @@ -2000,9 +2003,19 @@ enabled. This can be done, by using next commands: \makesubsubsection{configuremssql}{Database Connection} \ind{Microsoft SQL Server!Database Connection} -The configuration of Database Connection for a Microsoft SQL Server -is the same as the configuration for -ODBC compatible servers (see section~\ref{configureodbc}). +By default \ejabberd{} opens 10 connections to the database for each virtual host. +Use this option to modify the value: +\begin{verbatim} +{odbc_pool_size, 10}. +\end{verbatim} + +You can configure an interval to make a dummy SQL request +to keep alive the connections to the database. +The default value is 'undefined', so no keepalive requests are made. +Specify in seconds: for example 28800 means 8 hours. +\begin{verbatim} +{odbc_keepalive_interval, undefined}. +\end{verbatim} \makesubsubsection{mssqlauth}{Authentication} @@ -2010,8 +2023,7 @@ ODBC compatible servers (see section~\ref{configureodbc}). %TODO: not sure if this section is right!!!!!! -The configuration of Authentication for a Microsoft SQL Server -is the same as the configuration for +The configuration of Microsoft SQL Server is the same as the configuration of ODBC compatible servers (see section~\ref{odbcauth}). \makesubsubsection{mssqlstorage}{Storage} @@ -2218,9 +2230,6 @@ and LDAP server supports \makesubsubsection{ldapconnection}{Connection} -Two connections are established to the LDAP server per vhost, -one for authentication and other for regular calls. - Parameters: \begin{description} \titem{\{ldap\_servers, [Servers, ...]\}} \ind{options!ldap\_server}List of IP addresses or DNS names of your @@ -2560,8 +2569,8 @@ You can see which database backend each module needs by looking at the suffix: \end{itemize} If you want to, -it is possible to use a relational database to store pieces of -information. You can do this by changing the module name to a name with an +it is possible to use a relational database to store the tables created by some ejabberd modules. +You can do this by changing the module name to a name with an \term{\_odbc} suffix in \ejabberd{} config file. You can use a relational database for the following data: @@ -2574,6 +2583,7 @@ database for the following data: \item Users' VCARD: Use \term{mod\_vcard\_odbc} instead of \term{mod\_vcard}. \item Private XML storage: Use \term{mod\_private\_odbc} instead of \term{mod\_private}. \item User rules for blocking communications: Use \term{mod\_privacy\_odbc} instead of \term{mod\_privacy}. +\item Pub-Sub nodes, items and subscriptions: Use \term{mod\_pubsub\_odbc} instead of \term{mod\_pubsub}. \end{itemize} You can find more diff --git a/doc/http_post.md b/doc/http_post.md new file mode 100644 index 000000000..359fb2adf --- /dev/null +++ b/doc/http_post.md @@ -0,0 +1,146 @@ +# Managing pubsub nodes through HTTP Atompub # + + +## Configuration ## + +These options will be used by the service to know how to build URLs. Using the previous configuration items the service should be accessed through `http://notify.push.bbc.co.uk:5280/pshb///`. + +Also, in the ejabberd_http handler configuration, add the identified line. + + {5280, ejabberd_http, [ + http_poll, + web_admin, + {request_handlers, [{["pshb"], pshb_http}]} % this should be added + ]} + +It will automatically detect the version of mod_pubsub (odbc or mnesia) and call the appropriate module. + +## Important notice ## + +In the current version of the code, some security checks are not done : + + * node creation uses the default `all` access_createnode acl, not checking for the actual configuration. + + * most read operations are successfully executed without authentication. HOWEVER listing items can only be done when the node access_model is "open". In all other cases, the service returns 403. A finer grained authentication will be implemented. + + +## Usage example with cURL ## + +### Errors ### + +HTTP status codes are used as intended. Additionally, the XMPP error stanza can also be set in the body : + + $ curl -i -X POST -u cstar@localhost:encore -d @createnode.xml http://localhost:5280/pshb/localhost + HTTP/1.1 409 Conflict + Content-Type: text/html; charset=utf-8 + Content-Length: 95 + Content-type: application/xml + + + +or + + $ curl -i -X DELETE -u cstar@localhost:encore http://localhost:5280/pshb/localhost/princely_musings + HTTP/1.1 404 Not Found + Content-Type: text/html; charset=utf-8 + Content-Length: 101 + Content-type: application/xml + + + +### Getting the service document ### + +No authentication necessary. All nodes are listed. + + $ curl -i http://host:port/pshb/domain/ + +### Getting items from a node ### + +No authentication done, and all nodes are accessible. + + $ curl -i http://host:port/pshb/domain/node/ + + +### Posting a new item ### + + $ curl -u jid:password -i -X POST -d @entry.atom http://post:port/pshb/domain/node + +User ability to post is based on node configuration. + +### Editing a new item ### + + $ curl -u jid:password -i -X POST -d @entry.atom http://post:port/pshb/domain/node/itemid + +User ability to post is based on node configuration. + +### Deleting an item ### + + $ curl -u jid:password -i -X DELETE http://post:port/pshb/domain/node/itemid + +User ability to post is based on node configuration. + + +### Creating a new node ### + +An instant node can be created if server configuration allows: + + $ curl -X POST -u cstar@localhost:encore -d "" http://localhost:5280/pshb/localhost + +or + + $ curl -X POST -u cstar@localhost:encore -d "" http://localhost:5280/pshb/localhost + +configure element (as per XEP-60) can be passed in the pubsub body. + + $ cat createnode.xml + + + + http://jabber.org/protocol/pubsub#node_config + + Princely Musings (Atom) + 1028 + Atom + + + + $ curl -i -X POST -u cstar@localhost:encore -d @createnode.xml http://localhost:5280/pshb/localhost + HTTP/1.1 200 OK + Content-Length: 130 + Content-Type: application/xml + + + +### Editing a node configuration ### + + $ cat editnode.xml + + + + + http://jabber.org/protocol/pubsub#node_config + + Princely Musings (Atom) + 1 + 1 + 1 + 10 + 604800 + roster + + + + + + $ curl -i -X PUT -u cstar@localhost:encore -d @createnode.xml http://localhost:5280/pshb/localhost/princely_musings + + + +### Deleting a node ### + +A node is deleted by: + + $ curl -X DELETE -u cstar@localhost:encore http://localhost:5280/pshb/localhost/princely_musings + + + diff --git a/src/Makefile.in b/src/Makefile.in index 4e561813d..312983395 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/cache_tab.erl b/src/cache_tab.erl new file mode 100644 index 000000000..b05e78b0e --- /dev/null +++ b/src/cache_tab.erl @@ -0,0 +1,563 @@ +%%%------------------------------------------------------------------- +%%% File : cache_tab.erl +%%% Author : Evgeniy Khramtsov +%%% Description : Caching key-value table +%%% +%%% Created : 29 Aug 2010 by Evgeniy Khramtsov +%%% +%%% +%%% 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(cache_tab). + +-define(GEN_SERVER, gen_server). + +-behaviour(?GEN_SERVER). + +%% API +-export([start_link/4, new/2, delete/1, delete/3, lookup/3, + insert/4, info/2, tab2list/1, setopts/2, + dirty_lookup/3, dirty_insert/4, dirty_delete/3, + all/0, test/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("ejabberd.hrl"). + +-record(state, {tab = treap:empty(), + name, + size = 0, + owner, + max_size, + life_time, + warn, + hits = 0, + miss = 0, + procs_num, + cache_missed, + shrink_size}). + +-define(PROCNAME, ?MODULE). +-define(CALL_TIMEOUT, 60000). + +%% Defaults +-define(MAX_SIZE, 1000). +-define(WARN, true). +-define(CACHE_MISSED, true). +-define(LIFETIME, 600). %% 10 minutes + +%%==================================================================== +%% API +%%==================================================================== +start_link(Proc, Tab, Opts, Owner) -> + ?GEN_SERVER:start_link( + {local, Proc}, ?MODULE, [Tab, Opts, get_proc_num(), Owner], []). + +new(Tab, Opts) -> + Res = lists:flatmap( + fun(Proc) -> + Spec = {{Tab, Proc}, + {?MODULE, start_link, + [Proc, Tab, Opts, self()]}, + permanent, + brutal_kill, + worker, + [?MODULE]}, + case supervisor:start_child(cache_tab_sup, Spec) of + {ok, _Pid} -> + [ok]; + R -> + [R] + end + end, get_all_procs(Tab)), + case lists:filter(fun(ok) -> false; (_) -> true end, Res) of + [] -> + ok; + Err -> + {error, Err} + end. + +delete(Tab) -> + lists:foreach( + fun(Proc) -> + supervisor:terminate_child(cache_tab_sup, {Tab, Proc}), + supervisor:delete_child(cache_tab_sup, {Tab, Proc}) + end, get_all_procs(Tab)). + +delete(Tab, Key, F) -> + ?GEN_SERVER:call( + get_proc_by_hash(Tab, Key), {delete, Key, F}, ?CALL_TIMEOUT). + +dirty_delete(Tab, Key, F) -> + F(), + ?GEN_SERVER:call( + get_proc_by_hash(Tab, Key), {dirty_delete, Key}, ?CALL_TIMEOUT). + +lookup(Tab, Key, F) -> + ?GEN_SERVER:call( + get_proc_by_hash(Tab, Key), {lookup, Key, F}, ?CALL_TIMEOUT). + +dirty_lookup(Tab, Key, F) -> + Proc = get_proc_by_hash(Tab, Key), + case ?GEN_SERVER:call(Proc, {dirty_lookup, Key}, ?CALL_TIMEOUT) of + {ok, '$cached_mismatch'} -> + error; + {ok, Val} -> + {ok, Val}; + _ -> + case F() of + {ok, Val} -> + ?GEN_SERVER:call( + Proc, {dirty_insert, Key, Val}, ?CALL_TIMEOUT), + {ok, Val}; + _ -> + error + end + end. + +insert(Tab, Key, Val, F) -> + ?GEN_SERVER:call( + get_proc_by_hash(Tab, Key), {insert, Key, Val, F}, ?CALL_TIMEOUT). + +dirty_insert(Tab, Key, Val, F) -> + F(), + ?GEN_SERVER:call( + get_proc_by_hash(Tab, Key), {dirty_insert, Key, Val}, ?CALL_TIMEOUT). + +info(Tab, Info) -> + case lists:map( + fun(Proc) -> + ?GEN_SERVER:call(Proc, {info, Info}, ?CALL_TIMEOUT) + end, get_all_procs(Tab)) of + Res when Info == size -> + {ok, lists:sum(Res)}; + Res when Info == all -> + {ok, Res}; + Res when Info == ratio -> + {H, M} = lists:foldl( + fun({Hits, Miss}, {HitsAcc, MissAcc}) -> + {HitsAcc + Hits, MissAcc + Miss} + end, {0, 0}, Res), + {ok, [{hits, H}, {miss, M}]}; + _ -> + {error, badarg} + end. + +setopts(Tab, Opts) -> + lists:foreach( + fun(Proc) -> + ?GEN_SERVER:call(Proc, {setopts, Opts}, ?CALL_TIMEOUT) + end, get_all_procs(Tab)). + +tab2list(Tab) -> + lists:flatmap( + fun(Proc) -> + ?GEN_SERVER:call(Proc, tab2list, ?CALL_TIMEOUT) + end, get_all_procs(Tab)). + +all() -> + lists:usort( + [Tab || {{Tab, _}, _, _, _} <- supervisor:which_children(cache_tab_sup)]). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== +init([Tab, Opts, N, Pid]) -> + State = #state{procs_num = N, + owner = Pid, + name = Tab}, + {ok, do_setopts(State, Opts)}. + +handle_call({lookup, Key, F}, _From, #state{tab = T} = State) -> + case treap:lookup(Key, T) of + {ok, _Prio, Val} -> + Hits = State#state.hits, + NewState = treap_update(Key, Val, State#state{hits = Hits + 1}), + case Val of + '$cached_mismatch' -> + {reply, error, NewState}; + _ -> + {reply, {ok, Val}, NewState} + end; + _ -> + case catch F() of + {ok, Val} -> + Miss = State#state.miss, + NewState = treap_insert(Key, Val, State), + {reply, {ok, Val}, NewState#state{miss = Miss + 1}}; + {'EXIT', Reason} -> + print_error(lookup, [Key], Reason, State), + {reply, error, State}; + _ -> + Miss = State#state.miss, + NewState = State#state{miss = Miss + 1}, + if State#state.cache_missed -> + {reply, error, + treap_insert(Key, '$cached_mismatch', NewState)}; + true -> + {reply, error, NewState} + end + end + end; +handle_call({dirty_lookup, Key}, _From, #state{tab = T} = State) -> + case treap:lookup(Key, T) of + {ok, _Prio, Val} -> + Hits = State#state.hits, + NewState = treap_update(Key, Val, State#state{hits = Hits + 1}), + {reply, {ok, Val}, NewState}; + _ -> + Miss = State#state.miss, + NewState = State#state{miss = Miss + 1}, + if State#state.cache_missed -> + {reply, error, + treap_insert(Key, '$cached_mismatch', NewState)}; + true -> + {reply, error, NewState} + end + end; +handle_call({insert, Key, Val, F}, _From, #state{tab = T} = State) -> + case treap:lookup(Key, T) of + {ok, _Prio, Val} -> + {reply, ok, State}; + Res -> + case catch F() of + {'EXIT', Reason} -> + print_error(insert, [Key, Val], Reason, State), + {reply, ok, State}; + _ -> + NewState = case Res of + {ok, _, _} -> + treap_update(Key, Val, State); + _ -> + treap_insert(Key, Val, State) + end, + {reply, ok, NewState} + end + end; +handle_call({dirty_insert, Key, Val}, _From, #state{tab = T} = State) -> + case treap:lookup(Key, T) of + {ok, _Prio, Val} -> + {reply, ok, State}; + {ok, _, _} -> + {reply, ok, treap_update(Key, Val, State)}; + _ -> + {reply, ok, treap_insert(Key, Val, State)} + end; +handle_call({delete, Key, F}, _From, State) -> + NewState = treap_delete(Key, State), + case catch F() of + {'EXIT', Reason} -> + print_error(delete, [Key], Reason, State); + _ -> + ok + end, + {reply, ok, NewState}; +handle_call({dirty_delete, Key}, _From, State) -> + NewState = treap_delete(Key, State), + {reply, ok, NewState}; +handle_call({info, Info}, _From, State) -> + Res = case Info of + size -> + State#state.size; + ratio -> + {State#state.hits, State#state.miss}; + all -> + [{max_size, State#state.max_size}, + {life_time, State#state.life_time}, + {shrink_size, State#state.shrink_size}, + {size, State#state.size}, + {owner, State#state.owner}, + {hits, State#state.hits}, + {miss, State#state.miss}, + {cache_missed, State#state.cache_missed}, + {warn, State#state.warn}]; + _ -> + badarg + end, + {reply, Res, State}; +handle_call(tab2list, _From, #state{tab = T} = State) -> + Res = treap:fold( + fun({Key, _, Val}, Acc) -> + [{Key, Val}|Acc] + end, [], T), + {reply, Res, State}; +handle_call({setopts, Opts}, _From, State) -> + {reply, ok, do_setopts(State, Opts)}; +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +do_setopts(#state{procs_num = N} = State, Opts) -> + MaxSize = case {proplists:get_value(max_size, Opts), + State#state.max_size} of + {MS, _} when is_integer(MS), MS > 0 -> + round(MS/N); + {unlimited, _} -> + unlimited; + {_, undefined} -> + round(?MAX_SIZE/N); + {_, MS} -> + MS + end, + LifeTime = case {proplists:get_value(life_time, Opts), + State#state.life_time} of + {LT, _} when is_integer(LT), LT > 0 -> + LT*1000*1000; + {unlimited, _} -> + unlimited; + {_, undefined} -> + ?LIFETIME*1000*1000; + {_, LT} -> + LT + end, + ShrinkSize = case {proplists:get_value(shrink_size, Opts), + State#state.shrink_size} of + {SS, _} when is_integer(SS), SS > 0 -> + round(SS/N); + _ when is_integer(MaxSize) -> + round(MaxSize/2); + _ -> + unlimited + end, + Warn = case {proplists:get_value(warn, Opts), + State#state.warn} of + {true, _} -> + true; + {false, _} -> + false; + {_, undefined} -> + ?WARN; + {_, W} -> + W + end, + CacheMissed = case proplists:get_value( + cache_missed, Opts, State#state.cache_missed) of + false -> + false; + true -> + true; + _ -> + ?CACHE_MISSED + end, + State#state{max_size = MaxSize, + warn = Warn, + life_time = LifeTime, + cache_missed = CacheMissed, + shrink_size = ShrinkSize}. + +get_proc_num() -> + erlang:system_info(logical_processors). + +get_proc_by_hash(Tab, Term) -> + N = erlang:phash2(Term, get_proc_num()) + 1, + get_proc(Tab, N). + +get_proc(Tab, N) -> + list_to_atom(atom_to_list(?PROCNAME) ++ "_" ++ + atom_to_list(Tab) ++ "_" ++ integer_to_list(N)). + +get_all_procs(Tab) -> + [get_proc(Tab, N) || N <- lists:seq(1, get_proc_num())]. + +now_priority() -> + {MSec, Sec, USec} = now(), + -((MSec*1000000 + Sec)*1000000 + USec). + +treap_update(Key, Val, #state{tab = T} = State) -> + Priority = now_priority(), + NewT = treap:insert(Key, Priority, Val, T), + State#state{tab = NewT}. + +treap_insert(Key, Val, State) -> + State1 = clean_treap(State), + #state{size = Size} = State2 = shrink_treap(State1), + treap_update(Key, Val, State2#state{size = Size+1}). + +treap_delete(Key, #state{tab = T, size = Size} = State) -> + case treap:lookup(Key, T) of + {ok, _, _} -> + NewT = treap:delete(Key, T), + clean_treap(State#state{tab = NewT, size = Size-1}); + _ -> + State + end. + +clean_treap(#state{tab = T, size = Size, life_time = LifeTime} = State) -> + if is_integer(LifeTime) -> + Priority = now_priority(), + {Cleaned, NewT} = clean_treap(T, Priority + LifeTime, 0), + State#state{size = Size - Cleaned, tab = NewT}; + true -> + State + end. + +clean_treap(Treap, CleanPriority, N) -> + case treap:is_empty(Treap) of + true -> + {N, Treap}; + false -> + {_Key, Priority, _Value} = treap:get_root(Treap), + if Priority > CleanPriority -> + clean_treap(treap:delete_root(Treap), CleanPriority, N+1); + true -> + {N, Treap} + end + end. + +shrink_treap(#state{tab = T, + max_size = MaxSize, + shrink_size = ShrinkSize, + warn = Warn, + size = Size} = State) when Size >= MaxSize -> + if Warn -> + ?WARNING_MSG("shrinking table:~n" + "** Table: ~p~n" + "** Processes Number: ~p~n" + "** Max Size: ~p items~n" + "** Shrink Size: ~p items~n" + "** Life Time: ~p microseconds~n" + "** Hits/Miss: ~p/~p~n" + "** Owner: ~p~n" + "** Cache Missed: ~p~n" + "** Instruction: you have to tune cacheing options" + " if this message repeats too frequently", + [State#state.name, State#state.procs_num, + MaxSize, ShrinkSize, State#state.life_time, + State#state.hits, State#state.miss, + State#state.owner, State#state.cache_missed]); + true -> + ok + end, + {Shrinked, NewT} = shrink_treap(T, ShrinkSize, 0), + State#state{tab = NewT, size = Size - Shrinked}; +shrink_treap(State) -> + State. + +shrink_treap(T, ShrinkSize, ShrinkSize) -> + {ShrinkSize, T}; +shrink_treap(T, ShrinkSize, N) -> + case treap:is_empty(T) of + true -> + {N, T}; + false -> + shrink_treap(treap:delete_root(T), ShrinkSize, N+1) + end. + +print_error(Operation, Args, Reason, State) -> + ?ERROR_MSG("callback failed:~n" + "** Tab: ~p~n" + "** Owner: ~p~n" + "** Operation: ~p~n" + "** Args: ~p~n" + "** Reason: ~p", + [State#state.name, State#state.owner, + Operation, Args, Reason]). + +%%-------------------------------------------------------------------- +%%% Tests +%%-------------------------------------------------------------------- +-define(lookup, dirty_lookup). +-define(delete, dirty_delete). +-define(insert, dirty_insert). +%% -define(lookup, lookup). +%% -define(delete, delete). +%% -define(insert, insert). + +test() -> + LifeTime = 2, + ok = new(test_tbl, [{life_time, LifeTime}, {max_size, unlimited}]), + check([]), + ok = ?insert(test_tbl, "key", "value", fun() -> ok end), + check([{"key", "value"}]), + {ok, "value"} = ?lookup(test_tbl, "key", fun() -> error end), + check([{"key", "value"}]), + io:format("** waiting for ~p seconds to check if cleaner works fine...~n", + [LifeTime+1]), + timer:sleep(timer:seconds(LifeTime+1)), + ok = ?insert(test_tbl, "key1", "value1", fun() -> ok end), + check([{"key1", "value1"}]), + ok = ?delete(test_tbl, "key1", fun() -> ok end), + {ok, "value"} = ?lookup(test_tbl, "key", fun() -> {ok, "value"} end), + check([{"key", "value"}]), + ok = ?delete(test_tbl, "key", fun() -> ok end), + check([]), + %% io:format("** testing buggy callbacks...~n"), + %% delete(test_tbl, "key", fun() -> erlang:error(badarg) end), + %% insert(test_tbl, "key", "val", fun() -> erlang:error(badarg) end), + %% lookup(test_tbl, "key", fun() -> erlang:error(badarg) end), + check([]), + delete(test_tbl), + test1(). + +test1() -> + MaxSize = 10, + ok = new(test_tbl, [{max_size, MaxSize}, {shrink_size, 1}, {warn, false}]), + lists:foreach( + fun(N) -> + ok = ?insert(test_tbl, N, N, fun() -> ok end) + end, lists:seq(1, MaxSize*get_proc_num())), + {ok, MaxSize} = info(test_tbl, size), + delete(test_tbl), + io:format("** testing speed, this may take a while...~n"), + test2(1000), + test2(10000), + test2(100000), + test2(1000000). + +test2(Iter) -> + ok = new(test_tbl, [{max_size, unlimited}, {life_time, unlimited}]), + L = lists:seq(1, Iter), + T1 = now(), + lists:foreach( + fun(N) -> + ok = ?insert(test_tbl, N, N, fun() -> ok end) + end, L), + io:format("** average insert (size = ~p): ~p usec~n", + [Iter, round(timer:now_diff(now(), T1)/Iter)]), + T2 = now(), + lists:foreach( + fun(N) -> + {ok, N} = ?lookup(test_tbl, N, fun() -> ok end) + end, L), + io:format("** average lookup (size = ~p): ~p usec~n", + [Iter, round(timer:now_diff(now(), T2)/Iter)]), + {ok, Iter} = info(test_tbl, size), + delete(test_tbl). + +check(List) -> + Size = length(List), + {ok, Size} = info(test_tbl, size), + List = tab2list(test_tbl). diff --git a/src/cache_tab_sup.erl b/src/cache_tab_sup.erl new file mode 100644 index 000000000..a49593f5e --- /dev/null +++ b/src/cache_tab_sup.erl @@ -0,0 +1,53 @@ +%%%------------------------------------------------------------------- +%%% File : cache_tab_sup.erl +%%% Author : Evgeniy Khramtsov +%%% Description : Cache tables supervisor +%%% +%%% Created : 30 Aug 2010 by Evgeniy Khramtsov +%%% +%%% +%%% 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(cache_tab_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callbacks +-export([init/1]). + +-define(SERVER, ?MODULE). + +%%==================================================================== +%% API functions +%%==================================================================== +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +%%==================================================================== +%% Supervisor callbacks +%%==================================================================== +init([]) -> + {ok, {{one_for_one,10,1}, []}}. + +%%==================================================================== +%% Internal functions +%%==================================================================== diff --git a/src/configure b/src/configure index d515abc22..ff51ca020 100755 --- a/src/configure +++ b/src/configure @@ -1,13 +1,13 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.65 for ejabberd 2.1.x. +# Generated by GNU Autoconf 2.67 for ejabberd 2.1.x. # # Report bugs to . # # # Copyright (C) 1992, 1993, 1994, 1995, 1996, 1998, 1999, 2000, 2001, -# 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Free Software Foundation, -# Inc. +# 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 Free Software +# Foundation, Inc. # # # This configure script is free software; the Free Software Foundation @@ -319,7 +319,7 @@ $as_echo X"$as_dir" | test -d "$as_dir" && break done test -z "$as_dirs" || eval "mkdir $as_dirs" - } || test -d "$as_dir" || as_fn_error "cannot create directory $as_dir" + } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" } # as_fn_mkdir_p @@ -359,19 +359,19 @@ else fi # as_fn_arith -# as_fn_error ERROR [LINENO LOG_FD] -# --------------------------------- +# as_fn_error STATUS ERROR [LINENO LOG_FD] +# ---------------------------------------- # Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are # provided, also output the error to LOG_FD, referencing LINENO. Then exit the -# script with status $?, using 1 if that was 0. +# script with STATUS, using 1 if that was 0. as_fn_error () { - as_status=$?; test $as_status -eq 0 && as_status=1 - if test "$3"; then - as_lineno=${as_lineno-"$2"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - $as_echo "$as_me:${as_lineno-$LINENO}: error: $1" >&$3 + as_status=$1; test $as_status -eq 0 && as_status=1 + if test "$4"; then + as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi - $as_echo "$as_me: error: $1" >&2 + $as_echo "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error @@ -533,7 +533,7 @@ test -n "$DJDIR" || exec 7<&0 &1 # Name of the host. -# hostname on some systems (SVR3.2, Linux) returns a bogus exit status, +# hostname on some systems (SVR3.2, old GNU/Linux) returns a bogus exit status, # so uname gets run too. ac_hostname=`(hostname || uname -n) 2>/dev/null | sed 1q` @@ -616,6 +616,7 @@ nif full_xml transient_supervisors db_type +flash_hack roster_gateway_workaround hipe PAM_LIBS @@ -720,6 +721,7 @@ enable_pam with_pam enable_hipe enable_roster_gateway_workaround +enable_flash_hack enable_mssql enable_transient_supervisors enable_full_xml @@ -800,8 +802,9 @@ do fi case $ac_option in - *=*) ac_optarg=`expr "X$ac_option" : '[^=]*=\(.*\)'` ;; - *) ac_optarg=yes ;; + *=?*) ac_optarg=`expr "X$ac_option" : '[^=]*=\(.*\)'` ;; + *=) ac_optarg= ;; + *) ac_optarg=yes ;; esac # Accept the important Cygnus configure options, so we can diagnose typos. @@ -846,7 +849,7 @@ do ac_useropt=`expr "x$ac_option" : 'x-*disable-\(.*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error "invalid feature name: $ac_useropt" + as_fn_error $? "invalid feature name: $ac_useropt" ac_useropt_orig=$ac_useropt ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in @@ -872,7 +875,7 @@ do ac_useropt=`expr "x$ac_option" : 'x-*enable-\([^=]*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error "invalid feature name: $ac_useropt" + as_fn_error $? "invalid feature name: $ac_useropt" ac_useropt_orig=$ac_useropt ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in @@ -1076,7 +1079,7 @@ do ac_useropt=`expr "x$ac_option" : 'x-*with-\([^=]*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error "invalid package name: $ac_useropt" + as_fn_error $? "invalid package name: $ac_useropt" ac_useropt_orig=$ac_useropt ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in @@ -1092,7 +1095,7 @@ do ac_useropt=`expr "x$ac_option" : 'x-*without-\(.*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error "invalid package name: $ac_useropt" + as_fn_error $? "invalid package name: $ac_useropt" ac_useropt_orig=$ac_useropt ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in @@ -1122,8 +1125,8 @@ do | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*) x_libraries=$ac_optarg ;; - -*) as_fn_error "unrecognized option: \`$ac_option' -Try \`$0 --help' for more information." + -*) as_fn_error $? "unrecognized option: \`$ac_option' +Try \`$0 --help' for more information" ;; *=*) @@ -1131,7 +1134,7 @@ Try \`$0 --help' for more information." # Reject names that are not valid shell variable names. case $ac_envvar in #( '' | [0-9]* | *[!_$as_cr_alnum]* ) - as_fn_error "invalid variable name: \`$ac_envvar'" ;; + as_fn_error $? "invalid variable name: \`$ac_envvar'" ;; esac eval $ac_envvar=\$ac_optarg export $ac_envvar ;; @@ -1149,13 +1152,13 @@ done if test -n "$ac_prev"; then ac_option=--`echo $ac_prev | sed 's/_/-/g'` - as_fn_error "missing argument to $ac_option" + as_fn_error $? "missing argument to $ac_option" fi if test -n "$ac_unrecognized_opts"; then case $enable_option_checking in no) ;; - fatal) as_fn_error "unrecognized options: $ac_unrecognized_opts" ;; + fatal) as_fn_error $? "unrecognized options: $ac_unrecognized_opts" ;; *) $as_echo "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; esac fi @@ -1178,7 +1181,7 @@ do [\\/$]* | ?:[\\/]* ) continue;; NONE | '' ) case $ac_var in *prefix ) continue;; esac;; esac - as_fn_error "expected an absolute directory name for --$ac_var: $ac_val" + as_fn_error $? "expected an absolute directory name for --$ac_var: $ac_val" done # There might be people who depend on the old broken behavior: `$host' @@ -1192,8 +1195,8 @@ target=$target_alias if test "x$host_alias" != x; then if test "x$build_alias" = x; then cross_compiling=maybe - $as_echo "$as_me: WARNING: If you wanted to set the --build type, don't use --host. - If a cross compiler is detected then cross compile mode will be used." >&2 + $as_echo "$as_me: WARNING: if you wanted to set the --build type, don't use --host. + If a cross compiler is detected then cross compile mode will be used" >&2 elif test "x$build_alias" != "x$host_alias"; then cross_compiling=yes fi @@ -1208,9 +1211,9 @@ test "$silent" = yes && exec 6>/dev/null ac_pwd=`pwd` && test -n "$ac_pwd" && ac_ls_di=`ls -di .` && ac_pwd_ls_di=`cd "$ac_pwd" && ls -di .` || - as_fn_error "working directory cannot be determined" + as_fn_error $? "working directory cannot be determined" test "X$ac_ls_di" = "X$ac_pwd_ls_di" || - as_fn_error "pwd does not report name of working directory" + as_fn_error $? "pwd does not report name of working directory" # Find the source files, if location was not specified. @@ -1249,11 +1252,11 @@ else fi if test ! -r "$srcdir/$ac_unique_file"; then test "$ac_srcdir_defaulted" = yes && srcdir="$ac_confdir or .." - as_fn_error "cannot find sources ($ac_unique_file) in $srcdir" + as_fn_error $? "cannot find sources ($ac_unique_file) in $srcdir" fi ac_msg="sources are in $srcdir, but \`cd $srcdir' does not work" ac_abs_confdir=`( - cd "$srcdir" && test -r "./$ac_unique_file" || as_fn_error "$ac_msg" + cd "$srcdir" && test -r "./$ac_unique_file" || as_fn_error $? "$ac_msg" pwd)` # When building in place, set srcdir=. if test "$ac_abs_confdir" = "$ac_pwd"; then @@ -1293,7 +1296,7 @@ Configuration: --help=short display options specific to this package --help=recursive display the short help of all the included packages -V, --version display version information and exit - -q, --quiet, --silent do not print \`checking...' messages + -q, --quiet, --silent do not print \`checking ...' messages --cache-file=FILE cache test results in FILE [disabled] -C, --config-cache alias for \`--cache-file=config.cache' -n, --no-create do not create output files @@ -1368,6 +1371,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 @@ -1471,9 +1475,9 @@ test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF ejabberd configure 2.1.x -generated by GNU Autoconf 2.65 +generated by GNU Autoconf 2.67 -Copyright (C) 2009 Free Software Foundation, Inc. +Copyright (C) 2010 Free Software Foundation, Inc. This configure script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it. _ACEOF @@ -1589,7 +1593,7 @@ $as_echo "$ac_try_echo"; } >&5 mv -f conftest.er1 conftest.err fi $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; } >/dev/null && { + test $ac_status = 0; } > conftest.i && { test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" || test ! -s conftest.err }; then : @@ -1613,10 +1617,10 @@ fi ac_fn_c_check_header_mongrel () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - if { as_var=$3; eval "test \"\${$as_var+set}\" = set"; }; then : + if eval "test \"\${$3+set}\"" = set; then : { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 $as_echo_n "checking for $2... " >&6; } -if { as_var=$3; eval "test \"\${$as_var+set}\" = set"; }; then : +if eval "test \"\${$3+set}\"" = set; then : $as_echo_n "(cached) " >&6 fi eval ac_res=\$$3 @@ -1652,7 +1656,7 @@ if ac_fn_c_try_cpp "$LINENO"; then : else ac_header_preproc=no fi -rm -f conftest.err conftest.$ac_ext +rm -f conftest.err conftest.i conftest.$ac_ext { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_header_preproc" >&5 $as_echo "$ac_header_preproc" >&6; } @@ -1675,17 +1679,15 @@ $as_echo "$as_me: WARNING: $2: see the Autoconf documentation" >&2;} $as_echo "$as_me: WARNING: $2: section \"Present But Cannot Be Compiled\"" >&2;} { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $2: proceeding with the compiler's result" >&5 $as_echo "$as_me: WARNING: $2: proceeding with the compiler's result" >&2;} -( cat <<\_ASBOX -## --------------------------------------- ## +( $as_echo "## --------------------------------------- ## ## Report this to ejabberd@process-one.net ## -## --------------------------------------- ## -_ASBOX +## --------------------------------------- ##" ) | sed "s/^/$as_me: WARNING: /" >&2 ;; esac { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 $as_echo_n "checking for $2... " >&6; } -if { as_var=$3; eval "test \"\${$as_var+set}\" = set"; }; then : +if eval "test \"\${$3+set}\"" = set; then : $as_echo_n "(cached) " >&6 else eval "$3=\$ac_header_compiler" @@ -1749,7 +1751,7 @@ ac_fn_c_check_header_compile () as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 $as_echo_n "checking for $2... " >&6; } -if { as_var=$3; eval "test \"\${$as_var+set}\" = set"; }; then : +if eval "test \"\${$3+set}\"" = set; then : $as_echo_n "(cached) " >&6 else cat confdefs.h - <<_ACEOF >conftest.$ac_ext @@ -1817,7 +1819,7 @@ 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 -generated by GNU Autoconf 2.65. Invocation command line was +generated by GNU Autoconf 2.67. Invocation command line was $ $0 $@ @@ -1927,11 +1929,9 @@ trap 'exit_status=$? { echo - cat <<\_ASBOX -## ---------------- ## + $as_echo "## ---------------- ## ## Cache variables. ## -## ---------------- ## -_ASBOX +## ---------------- ##" echo # The following way of writing the cache mishandles newlines in values, ( @@ -1965,11 +1965,9 @@ $as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; ) echo - cat <<\_ASBOX -## ----------------- ## + $as_echo "## ----------------- ## ## Output variables. ## -## ----------------- ## -_ASBOX +## ----------------- ##" echo for ac_var in $ac_subst_vars do @@ -1982,11 +1980,9 @@ _ASBOX echo if test -n "$ac_subst_files"; then - cat <<\_ASBOX -## ------------------- ## + $as_echo "## ------------------- ## ## File substitutions. ## -## ------------------- ## -_ASBOX +## ------------------- ##" echo for ac_var in $ac_subst_files do @@ -2000,11 +1996,9 @@ _ASBOX fi if test -s confdefs.h; then - cat <<\_ASBOX -## ----------- ## + $as_echo "## ----------- ## ## confdefs.h. ## -## ----------- ## -_ASBOX +## ----------- ##" echo cat confdefs.h echo @@ -2059,7 +2053,12 @@ _ACEOF ac_site_file1=NONE ac_site_file2=NONE if test -n "$CONFIG_SITE"; then - ac_site_file1=$CONFIG_SITE + # We do not want a PATH search for config.site. + case $CONFIG_SITE in #(( + -*) ac_site_file1=./$CONFIG_SITE;; + */*) ac_site_file1=$CONFIG_SITE;; + *) ac_site_file1=./$CONFIG_SITE;; + esac elif test "x$prefix" != xNONE; then ac_site_file1=$prefix/share/config.site ac_site_file2=$prefix/etc/config.site @@ -2074,7 +2073,11 @@ do { $as_echo "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 $as_echo "$as_me: loading site script $ac_site_file" >&6;} sed 's/^/| /' "$ac_site_file" >&5 - . "$ac_site_file" + . "$ac_site_file" \ + || { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "failed to load site script $ac_site_file +See \`config.log' for more details" "$LINENO" 5 ; } fi done @@ -2150,7 +2153,7 @@ if $ac_cache_corrupted; then $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} { $as_echo "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 $as_echo "$as_me: error: changes in the environment can compromise the build" >&2;} - as_fn_error "run \`make distclean' and/or \`rm $cache_file' and start over" "$LINENO" 5 + as_fn_error $? "run \`make distclean' and/or \`rm $cache_file' and start over" "$LINENO" 5 fi ## -------------------- ## ## Main body of script. ## @@ -2465,8 +2468,8 @@ fi test -z "$CC" && { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error "no acceptable C compiler found in \$PATH -See \`config.log' for more details." "$LINENO" 5; } +as_fn_error $? "no acceptable C compiler found in \$PATH +See \`config.log' for more details" "$LINENO" 5 ; } # Provide some information about the compiler. $as_echo "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 @@ -2580,9 +2583,8 @@ sed 's/^/| /' conftest.$ac_ext >&5 { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -{ as_fn_set_status 77 -as_fn_error "C compiler cannot create executables -See \`config.log' for more details." "$LINENO" 5; }; } +as_fn_error 77 "C compiler cannot create executables +See \`config.log' for more details" "$LINENO" 5 ; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 $as_echo "yes" >&6; } @@ -2624,8 +2626,8 @@ done else { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error "cannot compute suffix of executables: cannot compile and link -See \`config.log' for more details." "$LINENO" 5; } +as_fn_error $? "cannot compute suffix of executables: cannot compile and link +See \`config.log' for more details" "$LINENO" 5 ; } fi rm -f conftest conftest$ac_cv_exeext { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5 @@ -2682,9 +2684,9 @@ $as_echo "$ac_try_echo"; } >&5 else { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error "cannot run C compiled programs. +as_fn_error $? "cannot run C compiled programs. If you meant to cross compile, use \`--host'. -See \`config.log' for more details." "$LINENO" 5; } +See \`config.log' for more details" "$LINENO" 5 ; } fi fi fi @@ -2735,8 +2737,8 @@ sed 's/^/| /' conftest.$ac_ext >&5 { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error "cannot compute suffix of object files: cannot compile -See \`config.log' for more details." "$LINENO" 5; } +as_fn_error $? "cannot compute suffix of object files: cannot compile +See \`config.log' for more details" "$LINENO" 5 ; } fi rm -f conftest.$ac_cv_objext conftest.$ac_ext fi @@ -2960,7 +2962,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu $as_echo_n "checking whether ${MAKE-make} sets \$(MAKE)... " >&6; } set x ${MAKE-make} ac_make=`$as_echo "$2" | sed 's/+/p/g; s/[^a-zA-Z0-9_]/_/g'` -if { as_var=ac_cv_prog_make_${ac_make}_set; eval "test \"\${$as_var+set}\" = set"; }; then : +if eval "test \"\${ac_cv_prog_make_${ac_make}_set+set}\"" = set; then : $as_echo_n "(cached) " >&6 else cat >conftest.make <<\_ACEOF @@ -2968,7 +2970,7 @@ SHELL = /bin/sh all: @echo '@@@%%%=$(MAKE)=@@@%%%' _ACEOF -# GNU make sometimes prints "make[1]: Entering...", which would confuse us. +# GNU make sometimes prints "make[1]: Entering ...", which would confuse us. case `${MAKE-make} -f conftest.make 2>/dev/null` in *@@@%%%=?*=@@@%%%*) eval ac_cv_prog_make_${ac_make}_set=yes;; @@ -3202,7 +3204,7 @@ fi if test "z$ERLC" = "z" || test "z$ERL" = "z"; then - as_fn_error "erlang not found" "$LINENO" 5 + as_fn_error $? "erlang not found" "$LINENO" 5 fi @@ -3260,15 +3262,15 @@ libpath(App) -> _EOF if ! $ERLC conftest.erl; then - as_fn_error "could not compile sample program" "$LINENO" 5 + as_fn_error $? "could not compile sample program" "$LINENO" 5 fi if ! $ERL -s conftest -noshell; then - as_fn_error "could not run sample program" "$LINENO" 5 + as_fn_error $? "could not run sample program" "$LINENO" 5 fi if ! test -f conftest.out; then - as_fn_error "erlang program was not properly executed, (conftest.out was not produced)" "$LINENO" 5 + as_fn_error $? "erlang program was not properly executed, (conftest.out was not produced)" "$LINENO" 5 fi # First line @@ -3497,7 +3499,7 @@ else # Broken: fails on valid input. continue fi -rm -f conftest.err conftest.$ac_ext +rm -f conftest.err conftest.i conftest.$ac_ext # OK, works on sane cases. Now check whether nonexistent headers # can be detected and how. @@ -3513,11 +3515,11 @@ else ac_preproc_ok=: break fi -rm -f conftest.err conftest.$ac_ext +rm -f conftest.err conftest.i conftest.$ac_ext done # Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped. -rm -f conftest.err conftest.$ac_ext +rm -f conftest.i conftest.err conftest.$ac_ext if $ac_preproc_ok; then : break fi @@ -3556,7 +3558,7 @@ else # Broken: fails on valid input. continue fi -rm -f conftest.err conftest.$ac_ext +rm -f conftest.err conftest.i conftest.$ac_ext # OK, works on sane cases. Now check whether nonexistent headers # can be detected and how. @@ -3572,18 +3574,18 @@ else ac_preproc_ok=: break fi -rm -f conftest.err conftest.$ac_ext +rm -f conftest.err conftest.i conftest.$ac_ext done # Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped. -rm -f conftest.err conftest.$ac_ext +rm -f conftest.i conftest.err conftest.$ac_ext if $ac_preproc_ok; then : else { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error "C preprocessor \"$CPP\" fails sanity check -See \`config.log' for more details." "$LINENO" 5; } +as_fn_error $? "C preprocessor \"$CPP\" fails sanity check +See \`config.log' for more details" "$LINENO" 5 ; } fi ac_ext=c @@ -3644,7 +3646,7 @@ esac done IFS=$as_save_IFS if test -z "$ac_cv_path_GREP"; then - as_fn_error "no acceptable grep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5 + as_fn_error $? "no acceptable grep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5 fi else ac_cv_path_GREP=$GREP @@ -3710,7 +3712,7 @@ esac done IFS=$as_save_IFS if test -z "$ac_cv_path_EGREP"; then - as_fn_error "no acceptable egrep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5 + as_fn_error $? "no acceptable egrep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5 fi else ac_cv_path_EGREP=$EGREP @@ -3842,8 +3844,7 @@ do : as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default " -eval as_val=\$$as_ac_Header - if test "x$as_val" = x""yes; then : +if eval test \"x\$"$as_ac_Header"\" = x"yes"; then : cat >>confdefs.h <<_ACEOF #define `$as_echo "HAVE_$ac_header" | $as_tr_cpp` 1 _ACEOF @@ -3911,7 +3912,7 @@ else fi if test $expat_found = no; then - as_fn_error "Could not find development files of Expat library" "$LINENO" 5 + as_fn_error $? "Could not find development files of Expat library" "$LINENO" 5 fi expat_save_CFLAGS="$CFLAGS" CFLAGS="$CFLAGS $EXPAT_CFLAGS" @@ -3932,7 +3933,7 @@ fi done if test $expat_found = no; then - as_fn_error "Could not find expat.h" "$LINENO" 5 + as_fn_error $? "Could not find expat.h" "$LINENO" 5 fi CFLAGS="$expat_save_CFLAGS" CPPFLAGS="$expat_save_CPPFLAGS" @@ -4469,7 +4470,7 @@ else fi if test $zlib_found = no; then - as_fn_error "Could not find development files of zlib library. Install them or disable \`ejabberd_zlib' with: --disable-ejabberd_zlib" "$LINENO" 5 + as_fn_error $? "Could not find development files of zlib library. Install them or disable \`ejabberd_zlib' with: --disable-ejabberd_zlib" "$LINENO" 5 fi zlib_save_CFLAGS="$CFLAGS" CFLAGS="$CFLAGS $ZLIB_CFLAGS" @@ -4490,7 +4491,7 @@ fi done if test $zlib_found = no; then - as_fn_error "Could not find zlib.h. Install it or disable \`ejabberd_zlib' with: --disable-ejabberd_zlib" "$LINENO" 5 + as_fn_error $? "Could not find zlib.h. Install it or disable \`ejabberd_zlib' with: --disable-ejabberd_zlib" "$LINENO" 5 fi CFLAGS="$zlib_save_CFLAGS" CPPFLAGS="$zlib_save_CPPFLAGS" @@ -4581,7 +4582,7 @@ else fi if test $pam_found = no; then - as_fn_error "Could not find development files of PAM library. Install them or disable \`pam' with: --disable-pam" "$LINENO" 5 + as_fn_error $? "Could not find development files of PAM library. Install them or disable \`pam' with: --disable-pam" "$LINENO" 5 fi pam_save_CFLAGS="$CFLAGS" CFLAGS="$CFLAGS $PAM_CFLAGS" @@ -4602,7 +4603,7 @@ fi done if test $pam_found = no; then - as_fn_error "Could not find security/pam_appl.h. Install it or disable \`pam' with: --disable-pam" "$LINENO" 5 + as_fn_error $? "Could not find security/pam_appl.h. Install it or disable \`pam' with: --disable-pam" "$LINENO" 5 fi CFLAGS="$pam_save_CFLAGS" CPPFLAGS="$pam_save_CPPFLAGS" @@ -4617,7 +4618,7 @@ if test "${enable_hipe+set}" = set; then : enableval=$enable_hipe; case "${enableval}" in yes) hipe=true ;; no) hipe=false ;; - *) as_fn_error "bad value ${enableval} for --enable-hipe" "$LINENO" 5 ;; + *) as_fn_error $? "bad value ${enableval} for --enable-hipe" "$LINENO" 5 ;; esac else hipe=false @@ -4630,7 +4631,7 @@ if test "${enable_roster_gateway_workaround+set}" = set; then : enableval=$enable_roster_gateway_workaround; case "${enableval}" in yes) roster_gateway_workaround=true ;; no) roster_gateway_workaround=false ;; - *) as_fn_error "bad value ${enableval} for --enable-roster-gateway-workaround" "$LINENO" 5 ;; + *) as_fn_error $? "bad value ${enableval} for --enable-roster-gateway-workaround" "$LINENO" 5 ;; esac else roster_gateway_workaround=false @@ -4638,12 +4639,25 @@ 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 yes) db_type=mssql ;; no) db_type=generic ;; - *) as_fn_error "bad value ${enableval} for --enable-mssql" "$LINENO" 5 ;; + *) as_fn_error $? "bad value ${enableval} for --enable-mssql" "$LINENO" 5 ;; esac else db_type=generic @@ -4656,7 +4670,7 @@ if test "${enable_transient_supervisors+set}" = set; then : enableval=$enable_transient_supervisors; case "${enableval}" in yes) transient_supervisors=true ;; no) transient_supervisors=false ;; - *) as_fn_error "bad value ${enableval} for --enable-transient_supervisors" "$LINENO" 5 ;; + *) as_fn_error $? "bad value ${enableval} for --enable-transient_supervisors" "$LINENO" 5 ;; esac else transient_supervisors=true @@ -4669,7 +4683,7 @@ if test "${enable_full_xml+set}" = set; then : enableval=$enable_full_xml; case "${enableval}" in yes) full_xml=true ;; no) full_xml=false ;; - *) as_fn_error "bad value ${enableval} for --enable-full-xml" "$LINENO" 5 ;; + *) as_fn_error $? "bad value ${enableval} for --enable-full-xml" "$LINENO" 5 ;; esac else full_xml=false @@ -4682,7 +4696,7 @@ if test "${enable_nif+set}" = set; then : enableval=$enable_nif; case "${enableval}" in yes) nif=true ;; no) nif=false ;; - *) as_fn_error "bad value ${enableval} for --enable-nif" "$LINENO" 5 ;; + *) as_fn_error $? "bad value ${enableval} for --enable-nif" "$LINENO" 5 ;; esac else nif=false @@ -4780,7 +4794,7 @@ done fi done if test x${have_openssl} != xyes; then - as_fn_error "Could not find development files of OpenSSL library. Install them or disable \`tls' with: --disable-tls" "$LINENO" 5 + as_fn_error $? "Could not find development files of OpenSSL library. Install them or disable \`tls' with: --disable-tls" "$LINENO" 5 fi @@ -4835,16 +4849,22 @@ fi ac_aux_dir= for ac_dir in "$srcdir" "$srcdir/.." "$srcdir/../.."; do - for ac_t in install-sh install.sh shtool; do - if test -f "$ac_dir/$ac_t"; then - ac_aux_dir=$ac_dir - ac_install_sh="$ac_aux_dir/$ac_t -c" - break 2 - fi - done + if test -f "$ac_dir/install-sh"; then + ac_aux_dir=$ac_dir + ac_install_sh="$ac_aux_dir/install-sh -c" + break + elif test -f "$ac_dir/install.sh"; then + ac_aux_dir=$ac_dir + ac_install_sh="$ac_aux_dir/install.sh -c" + break + elif test -f "$ac_dir/shtool"; then + ac_aux_dir=$ac_dir + ac_install_sh="$ac_aux_dir/shtool install -c" + break + fi done if test -z "$ac_aux_dir"; then - as_fn_error "cannot find install-sh, install.sh, or shtool in \"$srcdir\" \"$srcdir/..\" \"$srcdir/../..\"" "$LINENO" 5 + as_fn_error $? "cannot find install-sh, install.sh, or shtool in \"$srcdir\" \"$srcdir/..\" \"$srcdir/../..\"" "$LINENO" 5 fi # These three variables are undocumented and unsupported, @@ -4858,7 +4878,7 @@ ac_configure="$SHELL $ac_aux_dir/configure" # Please don't use this var. # Make sure we can run config.sub. $SHELL "$ac_aux_dir/config.sub" sun4 >/dev/null 2>&1 || - as_fn_error "cannot run $SHELL $ac_aux_dir/config.sub" "$LINENO" 5 + as_fn_error $? "cannot run $SHELL $ac_aux_dir/config.sub" "$LINENO" 5 { $as_echo "$as_me:${as_lineno-$LINENO}: checking build system type" >&5 $as_echo_n "checking build system type... " >&6; } @@ -4869,16 +4889,16 @@ else test "x$ac_build_alias" = x && ac_build_alias=`$SHELL "$ac_aux_dir/config.guess"` test "x$ac_build_alias" = x && - as_fn_error "cannot guess build type; you must specify one" "$LINENO" 5 + as_fn_error $? "cannot guess build type; you must specify one" "$LINENO" 5 ac_cv_build=`$SHELL "$ac_aux_dir/config.sub" $ac_build_alias` || - as_fn_error "$SHELL $ac_aux_dir/config.sub $ac_build_alias failed" "$LINENO" 5 + as_fn_error $? "$SHELL $ac_aux_dir/config.sub $ac_build_alias failed" "$LINENO" 5 fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_build" >&5 $as_echo "$ac_cv_build" >&6; } case $ac_cv_build in *-*-*) ;; -*) as_fn_error "invalid value of canonical build" "$LINENO" 5;; +*) as_fn_error $? "invalid value of canonical build" "$LINENO" 5 ;; esac build=$ac_cv_build ac_save_IFS=$IFS; IFS='-' @@ -4903,7 +4923,7 @@ else ac_cv_host=$ac_cv_build else ac_cv_host=`$SHELL "$ac_aux_dir/config.sub" $host_alias` || - as_fn_error "$SHELL $ac_aux_dir/config.sub $host_alias failed" "$LINENO" 5 + as_fn_error $? "$SHELL $ac_aux_dir/config.sub $host_alias failed" "$LINENO" 5 fi fi @@ -4911,7 +4931,7 @@ fi $as_echo "$ac_cv_host" >&6; } case $ac_cv_host in *-*-*) ;; -*) as_fn_error "invalid value of canonical host" "$LINENO" 5;; +*) as_fn_error $? "invalid value of canonical host" "$LINENO" 5 ;; esac host=$ac_cv_host ac_save_IFS=$IFS; IFS='-' @@ -4936,7 +4956,7 @@ else ac_cv_target=$ac_cv_host else ac_cv_target=`$SHELL "$ac_aux_dir/config.sub" $target_alias` || - as_fn_error "$SHELL $ac_aux_dir/config.sub $target_alias failed" "$LINENO" 5 + as_fn_error $? "$SHELL $ac_aux_dir/config.sub $target_alias failed" "$LINENO" 5 fi fi @@ -4944,7 +4964,7 @@ fi $as_echo "$ac_cv_target" >&6; } case $ac_cv_target in *-*-*) ;; -*) as_fn_error "invalid value of canonical target" "$LINENO" 5;; +*) as_fn_error $? "invalid value of canonical target" "$LINENO" 5 ;; esac target=$ac_cv_target ac_save_IFS=$IFS; IFS='-' @@ -5088,8 +5108,8 @@ fi if test "$cross_compiling" = yes; then : { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error "cannot run test program while cross compiling -See \`config.log' for more details." "$LINENO" 5; } +as_fn_error $? "cannot run test program while cross compiling +See \`config.log' for more details" "$LINENO" 5 ; } else cat > conftest.$ac_ext <<_ACEOF -module(conftest). @@ -5243,6 +5263,7 @@ DEFS=`sed -n "$ac_script" confdefs.h` ac_libobjs= ac_ltlibobjs= +U= for ac_i in : $LIBOBJS; do test "x$ac_i" = x: && continue # 1. Remove the extension, and $U if already installed. ac_script='s/\$U\././;s/\.o$//;s/\.obj$//' @@ -5404,19 +5425,19 @@ export LANGUAGE (unset CDPATH) >/dev/null 2>&1 && unset CDPATH -# as_fn_error ERROR [LINENO LOG_FD] -# --------------------------------- +# as_fn_error STATUS ERROR [LINENO LOG_FD] +# ---------------------------------------- # Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are # provided, also output the error to LOG_FD, referencing LINENO. Then exit the -# script with status $?, using 1 if that was 0. +# script with STATUS, using 1 if that was 0. as_fn_error () { - as_status=$?; test $as_status -eq 0 && as_status=1 - if test "$3"; then - as_lineno=${as_lineno-"$2"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - $as_echo "$as_me:${as_lineno-$LINENO}: error: $1" >&$3 + as_status=$1; test $as_status -eq 0 && as_status=1 + if test "$4"; then + as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi - $as_echo "$as_me: error: $1" >&2 + $as_echo "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error @@ -5612,7 +5633,7 @@ $as_echo X"$as_dir" | test -d "$as_dir" && break done test -z "$as_dirs" || eval "mkdir $as_dirs" - } || test -d "$as_dir" || as_fn_error "cannot create directory $as_dir" + } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" } # as_fn_mkdir_p @@ -5666,7 +5687,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # values after options handling. ac_log=" This file was extended by ejabberd $as_me 2.1.x, which was -generated by GNU Autoconf 2.65. Invocation command line was +generated by GNU Autoconf 2.67. Invocation command line was CONFIG_FILES = $CONFIG_FILES CONFIG_HEADERS = $CONFIG_HEADERS @@ -5719,10 +5740,10 @@ 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 -configured by $0, generated by GNU Autoconf 2.65, +configured by $0, generated by GNU Autoconf 2.67, with options \\"\$ac_cs_config\\" -Copyright (C) 2009 Free Software Foundation, Inc. +Copyright (C) 2010 Free Software Foundation, Inc. This config.status script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it." @@ -5737,11 +5758,16 @@ ac_need_defaults=: while test $# != 0 do case $1 in - --*=*) + --*=?*) ac_option=`expr "X$1" : 'X\([^=]*\)='` ac_optarg=`expr "X$1" : 'X[^=]*=\(.*\)'` ac_shift=: ;; + --*=) + ac_option=`expr "X$1" : 'X\([^=]*\)='` + ac_optarg= + ac_shift=: + ;; *) ac_option=$1 ac_optarg=$2 @@ -5763,6 +5789,7 @@ do $ac_shift case $ac_optarg in *\'*) ac_optarg=`$as_echo "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; + '') as_fn_error $? "missing file argument" ;; esac as_fn_append CONFIG_FILES " '$ac_optarg'" ac_need_defaults=false;; @@ -5773,7 +5800,7 @@ do ac_cs_silent=: ;; # This is an error. - -*) as_fn_error "unrecognized option: \`$1' + -*) as_fn_error $? "unrecognized option: \`$1' Try \`$0 --help' for more information." ;; *) as_fn_append ac_config_targets " $1" @@ -5836,7 +5863,7 @@ do "$make_odbc") CONFIG_FILES="$CONFIG_FILES $make_odbc" ;; "$make_ejabberd_zlib") CONFIG_FILES="$CONFIG_FILES $make_ejabberd_zlib" ;; - *) as_fn_error "invalid argument: \`$ac_config_target'" "$LINENO" 5;; + *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5 ;; esac done @@ -5872,7 +5899,7 @@ $debug || { tmp=./conf$$-$RANDOM (umask 077 && mkdir "$tmp") -} || as_fn_error "cannot create a temporary directory in ." "$LINENO" 5 +} || as_fn_error $? "cannot create a temporary directory in ." "$LINENO" 5 # Set up the scripts for CONFIG_FILES section. # No need to generate them if there are no CONFIG_FILES. @@ -5889,7 +5916,7 @@ if test "x$ac_cr" = x; then fi ac_cs_awk_cr=`$AWK 'BEGIN { print "a\rb" }' /dev/null` if test "$ac_cs_awk_cr" = "a${ac_cr}b"; then - ac_cs_awk_cr='\r' + ac_cs_awk_cr='\\r' else ac_cs_awk_cr=$ac_cr fi @@ -5903,18 +5930,18 @@ _ACEOF echo "$ac_subst_vars" | sed 's/.*/&!$&$ac_delim/' && echo "_ACEOF" } >conf$$subs.sh || - as_fn_error "could not make $CONFIG_STATUS" "$LINENO" 5 -ac_delim_num=`echo "$ac_subst_vars" | grep -c '$'` + as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 +ac_delim_num=`echo "$ac_subst_vars" | grep -c '^'` ac_delim='%!_!# ' for ac_last_try in false false false false false :; do . ./conf$$subs.sh || - as_fn_error "could not make $CONFIG_STATUS" "$LINENO" 5 + as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | grep -c X` if test $ac_delim_n = $ac_delim_num; then break elif $ac_last_try; then - as_fn_error "could not make $CONFIG_STATUS" "$LINENO" 5 + as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 else ac_delim="$ac_delim!$ac_delim _$ac_delim!! " fi @@ -6003,20 +6030,28 @@ if sed "s/$ac_cr//" < /dev/null > /dev/null 2>&1; then else cat fi < "$tmp/subs1.awk" > "$tmp/subs.awk" \ - || as_fn_error "could not setup config files machinery" "$LINENO" 5 + || as_fn_error $? "could not setup config files machinery" "$LINENO" 5 _ACEOF -# VPATH may cause trouble with some makes, so we remove $(srcdir), -# ${srcdir} and @srcdir@ from VPATH if srcdir is ".", strip leading and +# VPATH may cause trouble with some makes, so we remove sole $(srcdir), +# ${srcdir} and @srcdir@ entries from VPATH if srcdir is ".", strip leading and # trailing colons and then remove the whole line if VPATH becomes empty # (actually we leave an empty line to preserve line numbers). if test "x$srcdir" = x.; then - ac_vpsub='/^[ ]*VPATH[ ]*=/{ -s/:*\$(srcdir):*/:/ -s/:*\${srcdir}:*/:/ -s/:*@srcdir@:*/:/ -s/^\([^=]*=[ ]*\):*/\1/ + ac_vpsub='/^[ ]*VPATH[ ]*=[ ]*/{ +h +s/// +s/^/:/ +s/[ ]*$/:/ +s/:\$(srcdir):/:/g +s/:\${srcdir}:/:/g +s/:@srcdir@:/:/g +s/^:*// s/:*$// +x +s/\(=[ ]*\).*/\1/ +G +s/\n// s/^[^=]*=[ ]*$// }' fi @@ -6034,7 +6069,7 @@ do esac case $ac_mode$ac_tag in :[FHL]*:*);; - :L* | :C*:*) as_fn_error "invalid tag \`$ac_tag'" "$LINENO" 5;; + :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5 ;; :[FH]-) ac_tag=-:-;; :[FH]*) ac_tag=$ac_tag:$ac_tag.in;; esac @@ -6062,7 +6097,7 @@ do [\\/$]*) false;; *) test -f "$srcdir/$ac_f" && ac_f="$srcdir/$ac_f";; esac || - as_fn_error "cannot find input file: \`$ac_f'" "$LINENO" 5;; + as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5 ;; esac case $ac_f in *\'*) ac_f=`$as_echo "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac as_fn_append ac_file_inputs " '$ac_f'" @@ -6089,7 +6124,7 @@ $as_echo "$as_me: creating $ac_file" >&6;} case $ac_tag in *:-:* | *:-) cat >"$tmp/stdin" \ - || as_fn_error "could not create $ac_file" "$LINENO" 5 ;; + || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;; esac ;; esac @@ -6215,22 +6250,22 @@ s&@abs_top_builddir@&$ac_abs_top_builddir&;t t $ac_datarootdir_hack " eval sed \"\$ac_sed_extra\" "$ac_file_inputs" | $AWK -f "$tmp/subs.awk" >$tmp/out \ - || as_fn_error "could not create $ac_file" "$LINENO" 5 + || as_fn_error $? "could not create $ac_file" "$LINENO" 5 test -z "$ac_datarootdir_hack$ac_datarootdir_seen" && { ac_out=`sed -n '/\${datarootdir}/p' "$tmp/out"`; test -n "$ac_out"; } && { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' "$tmp/out"`; test -z "$ac_out"; } && { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \`datarootdir' -which seems to be undefined. Please make sure it is defined." >&5 +which seems to be undefined. Please make sure it is defined" >&5 $as_echo "$as_me: WARNING: $ac_file contains a reference to the variable \`datarootdir' -which seems to be undefined. Please make sure it is defined." >&2;} +which seems to be undefined. Please make sure it is defined" >&2;} rm -f "$tmp/stdin" case $ac_file in -) cat "$tmp/out" && rm -f "$tmp/out";; *) rm -f "$ac_file" && mv "$tmp/out" "$ac_file";; esac \ - || as_fn_error "could not create $ac_file" "$LINENO" 5 + || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;; @@ -6245,7 +6280,7 @@ _ACEOF ac_clean_files=$ac_clean_files_save test $ac_write_fail = 0 || - as_fn_error "write failure creating $CONFIG_STATUS" "$LINENO" 5 + as_fn_error $? "write failure creating $CONFIG_STATUS" "$LINENO" 5 # configure is writing to config.log, and then calls config.status. @@ -6266,7 +6301,7 @@ if test "$no_create" != yes; then exec 5>>config.log # Use ||, not &&, to avoid exiting from the if with $? = 1, which # would make configure fail if this is the last instruction. - $ac_cs_success || as_fn_exit $? + $ac_cs_success || as_fn_exit 1 fi if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 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 67653d241..ea85a9f26 100644 --- a/src/ejabberd.app +++ b/src/ejabberd.app @@ -2,7 +2,7 @@ {application, ejabberd, [{description, "ejabberd"}, - {vsn, "2.1.x"}, + {vsn, "2.2.x"}, {modules, [acl, adhoc, configure, diff --git a/src/ejabberd.hrl b/src/ejabberd.hrl index e1f0cfd37..fcc2aeac7 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_app.erl b/src/ejabberd_app.erl index 22279046f..8726805dd 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -68,6 +68,8 @@ start(normal, _Args) -> %ejabberd_debug:fprof_start(), maybe_add_nameservers(), start_modules(), + ejabberd_cluster:announce(), + ejabberd_node_groups:start(), ejabberd_listener:start_listeners(), ?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]), Sup; diff --git a/src/ejabberd_auth_anonymous.erl b/src/ejabberd_auth_anonymous.erl index 1f86e2cc5..94f46eb79 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 ]). @@ -61,12 +62,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. @@ -123,11 +128,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 @@ -136,7 +148,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) -> @@ -144,7 +156,7 @@ register_connection(SID, #jid{luser = LUser, lserver = LServer}, Info) -> case AuthModule == ?MODULE of true -> US = {LUser, LServer}, - mnesia:sync_dirty( + mnesia:async_dirty( fun() -> mnesia:write(#anonymous{us = US, sid=SID}) end); false -> @@ -157,6 +169,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; @@ -246,3 +262,11 @@ 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. diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index e8c37b740..570f4f636 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -35,13 +35,16 @@ %% External exports -export([start/2, stop/1, - start_link/2, + start_link/3, send_text/2, send_element/2, socket_type/0, get_presence/1, get_subscribed/1]). +%% API: +-export([add_rosteritem/3, del_rosteritem/2]). + %% gen_fsm callbacks -export([init/1, wait_for_stream/2, @@ -56,69 +59,14 @@ code_change/4, handle_info/3, terminate/3, - print_state/1 - ]). + print_state/1, + migrate/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, - lang, - 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_offline = false, - ack_enabled = false, - ack_counter = 0, - ack_queue = queue:new(), - ack_timer}). +-include("ejabberd_c2s.hrl"). %-define(DBGFSM, true). @@ -130,11 +78,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 @@ -149,6 +98,13 @@ "id='~s' from='~s'~s~s>" ). +-define(FLASH_STREAM_HEADER, + "" + "" + ). + -define(STREAM_TRAILER, ""). -define(INVALID_NS_ERR, ?SERR_INVALID_NAMESPACE). @@ -171,12 +127,17 @@ %%%---------------------------------------------------------------------- %%% 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. @@ -185,9 +146,18 @@ 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}). + stop(FsmRef) -> ?GEN_FSM:send_event(FsmRef, closed). +migrate(FsmRef, Node, After) -> + ?GEN_FSM:send_all_state_event(FsmRef, {migrate, Node, After}). + %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- @@ -199,7 +169,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 @@ -223,7 +193,12 @@ init([{SockMod, Socket}, Opts]) -> (_) -> false end, Opts), TLSOpts = [verify_none | TLSOpts1], - IP = peerip(SockMod, Socket), + 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 -> @@ -239,20 +214,47 @@ init([{SockMod, Socket}, Opts]) -> 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, + fsm_limit_opts = FSMLimitOpts}, + {ok, wait_for_stream, StateData, ?C2S_OPEN_TIMEOUT} + 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:open_session( + SID, + StateData#state.user, + StateData#state.server, + StateData#state.resource, + Priority, + Info), + NewStateData = StateData#state{sid = SID, socket_monitor = MRef}, + {ok, StateName, NewStateData}; + true -> + {ok, StateName, StateData#state{socket_monitor = MRef}} end. %% Return list of all available resources of contacts, @@ -266,15 +268,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 -> @@ -432,11 +444,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) -> @@ -515,13 +533,13 @@ wait_for_auth({xmlstreamelement, El}, StateData) -> jlib:jid_to_string(JID), AuthModule]), SID = {now(), self()}, Conn = get_conn_type(StateData), - Info = [{ip, StateData#state.ip}, {conn, Conn}, - {auth_module, AuthModule}], + %% 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), + %% 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, @@ -537,8 +555,7 @@ wait_for_auth({xmlstreamelement, El}, StateData) -> privacy_get_user_list, StateData#state.server, #userlist{}, [U, StateData#state.server]), - NewStateData = - StateData#state{ + NewStateData = StateData#state{ user = U, resource = R, jid = JID, @@ -548,8 +565,11 @@ wait_for_auth({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}); _ -> ?INFO_MSG( "(~w) Failed legacy authentication for ~s", @@ -641,8 +661,8 @@ wait_for_feature_request({xmlstreamelement, El}, StateData) -> Mech, ClientIn) of {ok, Props} -> - (StateData#state.sockmod):reset_stream( - StateData#state.socket), + catch (StateData#state.sockmod):reset_stream( + StateData#state.socket), send_element(StateData, {xmlelement, "success", [{"xmlns", ?NS_SASL}], []}), @@ -794,8 +814,8 @@ 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), + catch (StateData#state.sockmod):reset_stream( + StateData#state.socket), send_element(StateData, {xmlelement, "success", [{"xmlns", ?NS_SASL}], []}), @@ -919,7 +939,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 @@ -945,10 +965,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, @@ -956,8 +976,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]), @@ -1084,7 +1107,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, @@ -1100,6 +1123,10 @@ session_established2(El, StateData) -> "iq" -> case jlib:iq_query_info(NewEl) of #iq{xmlns = ?NS_PRIVACY} = IQ -> + 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 -> @@ -1108,7 +1135,7 @@ session_established2(El, StateData) -> ejabberd_hooks:run( user_send_packet, Server, - [FromJID, ToJID, NewEl]), + [StateData#state.debug, FromJID, ToJID, NewEl]), ejabberd_router:route( FromJID, ToJID, NewEl), StateData @@ -1116,7 +1143,7 @@ session_established2(El, StateData) -> "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; @@ -1154,6 +1181,17 @@ session_established2(El, StateData) -> %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- +handle_event({migrate, Node, After}, StateName, StateData) when Node /= node() -> + fsm_migrate(StateName, StateData, Node, After * 2); + +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, _}, StateName, StateData) -> ?DEBUG("cdata ping", []), NSD1 = change_reception(StateData, true), @@ -1458,7 +1496,7 @@ handle_info({route, From, To, Packet}, StateName, StateData) -> 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, NewState2); true -> @@ -1560,12 +1598,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 -> @@ -1675,7 +1732,14 @@ change_shaper(StateData, JID) -> 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, @@ -1693,6 +1757,15 @@ send_element(StateData, El) -> 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 = @@ -1781,16 +1854,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) -> @@ -2197,6 +2276,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 @@ -2353,7 +2433,7 @@ resend_offline_messages(#state{user = User, send_element(StateData, FixedPacket), ejabberd_hooks:run(user_receive_packet, StateData#state.server, - [StateData#state.jid, + [StateData#state.debug, StateData#state.jid, From, To, FixedPacket]); true -> ok @@ -2426,16 +2506,28 @@ 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), + case ejabberd_cluster:get_node({StateData#state.user, + StateData#state.server}) of + Node when Node == node() -> + Conn = get_conn_type(StateData), + Info = [{ip, StateData#state.ip}, {conn, Conn}, + {auth_module, StateData#state.auth_module}], + #state{user = U, server = S, resource = R, sid = SID} = StateData, + ejabberd_sm:open_session(SID, U, S, R, Info), + erlang:garbage_collect(), + case ejabberd_cluster:get_node_new({U, S}) of + Node -> + ok; + NewNode -> + After = ejabberd_cluster:rehash_timeout(), + migrate(self(), NewNode, After) + end, + fsm_next_state(StateName, PackedStateData); + 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 @@ -2444,6 +2536,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) -> @@ -3180,3 +3276,28 @@ 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, + + "\n" + "\n" + "\n" + " \n" + "\n\0". diff --git a/src/ejabberd_c2s.hrl b/src/ejabberd_c2s.hrl new file mode 100644 index 000000000..381e042ba --- /dev/null +++ b/src/ejabberd_c2s.hrl @@ -0,0 +1,85 @@ +%%%---------------------------------------------------------------------- +%%% +%%% 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, + 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_offline = false, + ack_enabled = false, + ack_counter = 0, + ack_queue = queue:new(), + ack_timer}). \ No newline at end of file diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl index ab3271b80..e575d7c40 100644 --- a/src/ejabberd_captcha.erl +++ b/src/ejabberd_captcha.erl @@ -35,7 +35,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --export([create_captcha/6, build_captcha_html/2, check_captcha/2, +-export([create_captcha/5, build_captcha_html/2, check_captcha/2, process_reply/1, process/2, is_feature_available/0]). -include("jlib.hrl"). @@ -48,19 +48,11 @@ -define(CAPTCHA_TEXT(Lang), translate:translate(Lang, "Enter the text you see")). -define(CAPTCHA_LIFETIME, 120000). % two minutes +-define(RPC_TIMEOUT, 5000). -record(state, {}). -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 %%==================================================================== @@ -71,11 +63,12 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -create_captcha(Id, SID, From, To, Lang, Args) - when is_list(Id), is_list(Lang), is_list(SID), +create_captcha(SID, From, To, Lang, Args) + when is_list(Lang), is_list(SID), is_record(From, jid), is_record(To, jid) -> case create_image() of {ok, Type, Key, Image} -> + 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", @@ -101,13 +94,9 @@ create_captcha(Id, SID, From, To, Lang, 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, [Body, OOB, Captcha, Data]}; - _Err -> - error - end; + ets:insert(captcha, #captcha{id=Id, pid=self(), key=Key, + tref=Tref, args=Args}), + {ok, Id, [Body, OOB, Captcha, Data]}; _Err -> error end. @@ -119,8 +108,8 @@ create_captcha(Id, SID, From, To, Lang, Args) %% 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"}, @@ -150,21 +139,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 -> - Pid ! {captcha_succeed, Args}, - captcha_valid; - true -> - Pid ! {captcha_failed, Args}, - 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, "captcha", _, _} = El) -> case xml:get_subtag(El, "x") of @@ -175,20 +168,14 @@ process_reply({xmlelement, "captcha", _, _} = El) -> case {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 -> - Pid ! {captcha_succeed, Args}, - ok; - true -> - Pid ! {captcha_failed, Args}, - {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 @@ -209,8 +196,8 @@ process(_Handlers, #request{method='GET', lang=Lang, path=[_, Id]}) -> end; process(_Handlers, #request{method='GET', path=[_, Id, "image"]}) -> - case mnesia:dirty_read(captcha, Id) of - [#captcha{key=Key}] -> + case lookup_captcha(Id) of + {ok, #captcha{key=Key}} -> case create_image(Key) of {ok, Type, _, Img} -> {200, @@ -249,10 +236,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{}}. @@ -264,13 +249,13 @@ 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}] -> - Pid ! {captcha_failed, Args}, - mnesia:delete({captcha, Id}); - _ -> - ok - end), + case ets:lookup(captcha, Id) of + [#captcha{args=Args, pid=Pid}] -> + Pid ! {captcha_failed, Args}, + ets:delete(captcha, Id); + _ -> + ok + end, {noreply, State}; handle_info(_Info, State) -> @@ -414,3 +399,43 @@ check_captcha_setup() -> false -> 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 -> + Pid ! {captcha_succeed, Args}, + captcha_valid; + true -> + Pid ! {captcha_failed, Args}, + captcha_non_valid + end; + _ -> + captcha_not_found + end. diff --git a/src/ejabberd_cluster.erl b/src/ejabberd_cluster.erl new file mode 100644 index 000000000..190e35202 --- /dev/null +++ b/src/ejabberd_cluster.erl @@ -0,0 +1,177 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_cluster.erl +%%% Author : Evgeniy Khramtsov +%%% Description : +%%% +%%% Created : 2 Apr 2010 by Evgeniy Khramtsov +%%%------------------------------------------------------------------- +-module(ejabberd_cluster). + +-behaviour(gen_server). + +%% API +-export([start_link/0, get_node/1, get_node_new/1, announce/0, + node_id/0, get_node_by_id/1, get_nodes/0, rehash_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, 30000). + +-record(state, {}). + +%%==================================================================== +%% API +%%==================================================================== +start_link() -> + gen_server:start_link({local, ?MODULE}, ?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() -> + gen_server:call(?MODULE, announce, infinity). + +node_id() -> + integer_to_list(erlang:phash2(node())). + +rehash_timeout() -> + ?REHASH_TIMEOUT. + +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. + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== +init([]) -> + 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 = mnesia:system_info(running_db_nodes), + OtherNodes = case AllNodes of + [_] -> + AllNodes; + _ -> + AllNodes -- [node()] + end, + append_nodes(?HASHTBL, OtherNodes), + append_nodes(?HASHTBL_NEW, AllNodes), + {ok, #state{}}. + +handle_call(announce, _From, State) -> + case mnesia:system_info(running_db_nodes) of + [_MyNode] -> + ok; + Nodes -> + OtherNodes = Nodes -- [node()], + lists:foreach( + fun(Node) -> + {?MODULE, Node} ! {node_ready, node()} + end, OtherNodes), + ?INFO_MSG("waiting for migration from nodes: ~w", + [OtherNodes]), + timer:sleep(?REHASH_TIMEOUT), + append_node(?HASHTBL, node()) + end, + {reply, ok, State}; +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({node_ready, Node}, State) -> + ?INFO_MSG("node ~p is ready, starting migration", [Node]), + append_node(?HASHTBL_NEW, Node), + ejabberd_hooks:run(node_hash_update, [?REHASH_TIMEOUT]), + timer:sleep(?REHASH_TIMEOUT), + ?INFO_MSG("adding node ~p to hash", [Node]), + append_node(?HASHTBL, 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_frontend_socket.erl b/src/ejabberd_frontend_socket.erl index 5ff4d08b4..88d77aed9 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 a179dfda4..0465dedba 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"). @@ -225,6 +226,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 @@ -239,11 +243,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} -> ?INFO_MSG("(~w) Failed TCP accept: ~w", [ListenSocket, Reason]), - accept(ListenSocket, Module, Opts) + accept(ListenSocket, Module, Opts, NewInterval) end. udp_recv(Socket, Module, Opts) -> @@ -471,3 +475,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 da1dadc0e..3c9f5d3da 100644 --- a/src/ejabberd_local.erl +++ b/src/ejabberd_local.erl @@ -125,7 +125,7 @@ route(From, To, Packet) -> route_iq(From, To, #iq{type = Type} = IQ, F) 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), jlib:iq_to_xml(IQ#iq{id = ID}); @@ -136,10 +136,10 @@ route_iq(From, To, #iq{type = Type} = IQ, F) when is_function(F) -> register_iq_response_handler(_Host, ID, Module, Function) -> TRef = erlang:start_timer(?IQ_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}. @@ -181,11 +181,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{}}. %%-------------------------------------------------------------------- @@ -257,7 +255,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}. @@ -312,40 +310,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 fc1b4ded5..055b96aef 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,7 +41,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {}). +-record(state, {groups = []}). %%==================================================================== %% API @@ -49,6 +50,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, [], []). @@ -81,30 +91,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} | @@ -144,7 +143,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 a3d0a8af8..0482a27dc 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) -> gen_server:call(Pid, reset_stream). -starttls(Pid, TLSSocket) -> - gen_server:call(Pid, {starttls, TLSSocket}). +starttls(Pid, TLSOpts) -> + starttls(Pid, TLSOpts, undefined). -compress(Pid, ZlibSocket) -> - gen_server:call(Pid, {compress, ZlibSocket}). +starttls(Pid, TLSOpts, Data) -> + gen_server:call(Pid, {starttls, TLSOpts, Data}). + +compress(Pid, Data) -> + gen_server:call(Pid, {compress, Data}). become_controller(Pid, C2SPid) -> gen_server: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 @@ -345,3 +430,21 @@ close_stream(undefined) -> ok; 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. diff --git a/src/ejabberd_router.erl b/src/ejabberd_router.erl index 87ba99094..ba4072cfa 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]), @@ -413,4 +445,3 @@ update_tables() -> false -> ok end. - diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index 8ab520f56..3e5807eee 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -40,7 +40,8 @@ dirty_get_connections/0, allow_host/2, incoming_s2s_number/0, - outgoing_s2s_number/0 + outgoing_s2s_number/0, + migrate/1 ]). %% gen_server callbacks @@ -94,30 +95,63 @@ remove_connection(FromTo, Pid, Key) -> end. have_connection(FromTo) -> - case catch mnesia:dirty_read(s2s, FromTo) of - [_] -> - true; - _ -> - false + case ejabberd_cluster:get_node(FromTo) of + Node when Node == node() -> + case mnesia:dirty_read(s2s, FromTo) of + [_] -> + true; + _ -> + false + end; + Node -> + case catch rpc:call(Node, mnesia, dirty_read, + [s2s, FromTo], 5000) of + [_] -> + true; + _ -> + false + end 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 ejabberd_cluster:get_node(FromTo) 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]; - _ -> - [] + case ejabberd_cluster:get_node(FromTo) of + Node when Node == node() -> + case catch mnesia:dirty_read(s2s, FromTo) of + L when is_list(L) -> + [Connection#s2s.pid || Connection <- L]; + _ -> + [] + end; + Node -> + case catch rpc:call(Node, mnesia, dirty_read, + [s2s, FromTo], 5000) of + L when is_list(L) -> + [Connection#s2s.pid || Connection <- L]; + _ -> + [] + end end. try_register(FromTo) -> @@ -148,7 +182,33 @@ 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()). + +migrate(After) -> + Ss = mnesia:dirty_select( + s2s, + [{#s2s{fromto = '$1', pid = '$2', _ = '_'}, + [], + ['$$']}]), + lists:foreach( + fun([FromTo, Pid]) -> + case ejabberd_cluster:get_node_new(FromTo) of + Node when Node /= node() -> + ejabberd_s2s_out:stop_connection(Pid, After * 2); + _ -> + ok + end + end, Ss). %%==================================================================== %% gen_server callbacks @@ -163,10 +223,11 @@ 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_hooks:add(node_hash_update, ?MODULE, migrate, 100), ejabberd_commands:register_commands(commands()), {ok, #state{}}. @@ -198,9 +259,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} -> @@ -221,6 +279,7 @@ handle_info(_Info, State) -> %% The return value is ignored. %%-------------------------------------------------------------------- terminate(_Reason, _State) -> + ejabberd_hooks:delete(node_hash_update, ?MODULE, migrate, 100), ejabberd_commands:unregister_commands(commands()), ok. @@ -234,22 +293,18 @@ 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]), + FromTo = {From#jid.lserver, To#jid.lserver}, + case ejabberd_cluster:get_node(FromTo) of + Node when Node == node() -> + do_route1(From, To, Packet); + Node -> + {?MODULE, Node} ! {route, From, To, Packet} + end. + +do_route1(From, To, Packet) -> case find_connection(From, To) of {atomic, Pid} when is_pid(Pid) -> ?DEBUG("sending to process ~p~n", [Pid]), @@ -482,6 +537,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 a1c641280..658786962 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, @@ -44,6 +45,7 @@ wait_for_features/2, wait_for_auth_result/2, wait_for_starttls_proceed/2, + relay_to_bridge/2, reopen_socket/2, wait_before_retry/2, stream_established/2, @@ -51,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"). @@ -72,6 +74,7 @@ myname, server, queue, delay_to_retry = undefined_delay, new = false, verify = false, + bridge, timer}). %%-define(DBGFSM, true). @@ -84,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. @@ -126,6 +130,7 @@ %%% API %%%---------------------------------------------------------------------- start(From, Host, Type) -> + Node = ejabberd_cluster:get_node({From, Host}), ?SUPERVISOR_START. start_link(From, Host, Type) -> @@ -136,7 +141,10 @@ start_connection(Pid) -> p1_fsm:send_event(Pid, init). stop_connection(Pid) -> - p1_fsm:send_event(Pid, stop). + p1_fsm:send_event(Pid, closed). + +stop_connection(Pid, Timeout) -> + p1_fsm:send_all_state_event(Pid, {closed, Timeout}). %%%---------------------------------------------------------------------- %%% Callback functions from p1_fsm @@ -228,8 +236,19 @@ open_socket(init, StateData) -> {error, _Reason} -> ?INFO_MSG("s2s connection: ~s -> ~s (remote server not found)", [StateData#state.myname, StateData#state.server]), - wait_before_reconnect(StateData) - %%{stop, normal, StateData} + case ejabberd_hooks:run_fold(find_s2s_bridge, + undefined, + [StateData#state.myname, + StateData#state.server]) of + {Mod, Fun, Type} -> + ?INFO_MSG("found a bridge to ~s for: ~s -> ~s", + [Type, StateData#state.myname, + StateData#state.server]), + NewStateData = StateData#state{bridge={Mod, Fun}}, + {next_state, relay_to_bridge, NewStateData}; + _ -> + wait_before_reconnect(StateData) + end end; open_socket(stop, StateData) -> ?INFO_MSG("s2s connection: ~s -> ~s (stopped in open socket)", @@ -677,6 +696,15 @@ reopen_socket(closed, StateData) -> wait_before_retry(_Event, StateData) -> {next_state, wait_before_retry, StateData, ?FSMTIMEOUT}. +relay_to_bridge(stop, StateData) -> + wait_before_reconnect(StateData); +relay_to_bridge(closed, StateData) -> + ?INFO_MSG("relay to bridge: ~s -> ~s (closed)", + [StateData#state.myname, StateData#state.server]), + {stop, normal, StateData}; +relay_to_bridge(_Event, StateData) -> + {next_state, relay_to_bridge, StateData}. + stream_established({xmlstreamelement, El}, StateData) -> ?DEBUG("s2S stream established", []), case is_verify_res(El) of @@ -747,6 +775,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)}. @@ -827,6 +858,19 @@ handle_info({send_element, El}, StateName, StateData) -> wait_before_retry -> bounce_element(El, ?ERR_REMOTE_SERVER_NOT_FOUND), {next_state, StateName, StateData}; + relay_to_bridge -> + %% In this state we relay all outbound messages + %% to a foreign protocol bridge such as SMTP, SIP, etc. + {Mod, Fun} = StateData#state.bridge, + ?DEBUG("relaying stanza via ~p:~p/1", [Mod, Fun]), + case catch Mod:Fun(El) of + {'EXIT', Reason} -> + ?ERROR_MSG("Error while relaying to bridge: ~p", [Reason]), + bounce_element(El, ?ERR_INTERNAL_SERVER_ERROR), + wait_before_reconnect(StateData); + _ -> + {next_state, StateName, StateData} + end; _ -> Q = queue:in(El, StateData#state.queue), {next_state, StateName, StateData#state{queue = Q}, diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index bd07bfea8..942f0fa4d 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -353,6 +353,9 @@ handle_info({route, From, To, Packet}, StateName, StateData) -> Err = jlib:make_error_reply(Packet, ?ERR_NOT_ALLOWED), ejabberd_router:route_error(To, From, Err, Packet) end, + {next_state, StateName, StateData}; +handle_info(Info, StateName, StateData) -> + ?ERROR_MSG("Unexpected info: ~p", [Info]), {next_state, StateName, StateData}. diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 4fc8123e7..e98ccf338 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -32,7 +32,11 @@ %% 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, check_in_subscription/6, bounce_offline_message/3, disconnect_removed_user/2, @@ -43,6 +47,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, @@ -53,7 +58,8 @@ user_resources/2, get_session_pid/3, get_user_info/3, - get_user_ip/3 + get_user_ip/3, + migrate/1 ]). %% gen_server callbacks @@ -66,7 +72,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 @@ -92,28 +97,37 @@ 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 = do_close_session(SID, User, Server, Resource), + 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, User, Server, Resource), + 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, User, Server, Resource) -> Info = case mnesia:dirty_read({session, SID}) of [] -> []; [#session{info=I}] -> I end, F = fun() -> - mnesia:delete({session, SID}), - mnesia:dirty_update_counter(session_counter, - jlib:nameprep(Server), -1) + mnesia:delete({session, SID}) 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]). + Info. check_in_subscription(Acc, User, Server, _JID, _Type, _Reason) -> case ejabberd_auth:is_user_exists(User, Server) of @@ -138,11 +152,17 @@ 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) -> @@ -150,12 +170,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) -> @@ -163,15 +189,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) -> @@ -194,26 +226,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, @@ -221,19 +264,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}. @@ -243,6 +291,21 @@ register_iq_handler(Host, XMLNS, Module, Fun, Opts) -> unregister_iq_handler(Host, XMLNS) -> ejabberd_sm ! {unregister_iq_handler, Host, XMLNS}. +migrate(After) -> + Ss = mnesia:dirty_select( + session, + [{#session{us = '$1', sid = {'_', '$2'}, _ = '_'}, + [], + ['$$']}]), + lists:foreach( + fun([US, Pid]) -> + case ejabberd_cluster:get_node_new(US) of + Node when Node /= node() -> + ejabberd_c2s:migrate(Pid, Node, After); + _ -> + ok + end + end, Ss). %%==================================================================== %% gen_server callbacks @@ -259,16 +322,13 @@ 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_hash_update, ?MODULE, migrate, 100), lists:foreach( fun(Host) -> ejabberd_hooks:add(roster_in_subscription, Host, @@ -319,9 +379,6 @@ handle_info({route, From, To, Packet}, State) -> ok end, {noreply, State}; -handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> - recount_session_table(Node), - {noreply, State}; handle_info({register_iq_handler, Host, XMLNS, Module, Function}, State) -> ets:insert(sm_iqtable, {{XMLNS, Host}, Module, Function}), {noreply, State}; @@ -348,6 +405,7 @@ handle_info(_Info, State) -> %% The return value is ignored. %%-------------------------------------------------------------------- terminate(_Reason, _State) -> + ejabberd_hooks:delete(node_hash_update, ?MODULE, migrate, 100), ejabberd_commands:unregister_commands(commands()), ok. @@ -377,38 +435,20 @@ set_session(SID, User, Server, Resource, Priority, 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). - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 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, @@ -795,4 +835,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 25ff64c27..a6cf0d0fe 100644 --- a/src/ejabberd_socket.erl +++ b/src/ejabberd_socket.erl @@ -44,6 +44,7 @@ get_peer_certificate/1, get_verify_result/1, close/1, + change_controller/2, sockname/1, peername/1]). -include("ejabberd.hrl"). @@ -129,29 +130,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) -> @@ -160,10 +151,25 @@ 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). + %% 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]), @@ -225,3 +231,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 f0119326a..d48deeaef 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, @@ -177,9 +178,23 @@ init([]) -> infinity, supervisor, [ejabberd_tmp_sup]}, + Cluster = + {ejabberd_cluster, + {ejabberd_cluster, start_link, []}, + permanent, + brutal_kill, + worker, + [ejabberd_cluster]}, + CacheTabSupervisor = + {cache_tab_sup, + {cache_tab_sup, start_link, []}, + permanent, + infinity, + supervisor, + [cache_tab_sup]}, {ok, {{one_for_one, 10, 1}, [Hooks, - NodeGroups, + Cluster, SystemMonitor, Router, SM, @@ -196,6 +211,7 @@ init([]) -> IQSupervisor, STUNSupervisor, FrontendSocketSupervisor, + CacheTabSupervisor, Listener]}}. diff --git a/src/ejabberdctl.template b/src/ejabberdctl.template index fecdecc86..a1e7902c7 100644 --- a/src/ejabberdctl.template +++ b/src/ejabberdctl.template @@ -136,6 +136,7 @@ export EXEC_CMD # start server start () { + check_start $EXEC_CMD "$ERL \ $NAME $ERLANG_NODE \ -noinput -detached \ @@ -182,6 +183,7 @@ debug () # start interactive server live () { + check_start echo "--------------------------------------------------------------------" echo "" echo "IMPORTANT: ejabberd is going to start in LIVE (interactive) mode." @@ -210,6 +212,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 "" @@ -330,6 +339,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 -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() { @@ -359,6 +388,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 f6b552c59..60cd72c88 100644 --- a/src/expat_erl.c +++ b/src/expat_erl.c @@ -138,6 +138,35 @@ static int expat_erl_control(ErlDrvData drv_data, case PARSE_COMMAND: case PARSE_FINAL_COMMAND: ei_x_new_with_version(&event_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, " + 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 +%%% Description : +%%% +%%% Created : 11 Sep 2008 by Christophe Romain +%%%------------------------------------------------------------------- +-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 +%%% Purpose : Provide a common API for inets / lhttpc / ibrowse +%%% Created : 29 Jul 2010 by Emilio Bustos +%%% +%%% +%%% 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 fe844be02..cb89f91a5 100644 --- a/src/jlib.hrl +++ b/src/jlib.hrl @@ -67,6 +67,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"). 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/mod_admin_p1.erl b/src/mod_admin_p1.erl new file mode 100644 index 000000000..18e391027 --- /dev/null +++ b/src/mod_admin_p1.erl @@ -0,0 +1,1166 @@ +%%%------------------------------------------------------------------- +%%% 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 +%%% +%%% +%%% 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, + link_contacts/4, + unlink_contacts/2, + link_contacts/5, 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_notification/6, %% ON + 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. " + "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" + "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 an entry for a user 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" + "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 = 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" + "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\n" + "This mechanism bypass 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}, {jid2, string}, {nick2, 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 userd 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}, {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_notification, tags = [stanza], + desc = "Send ON notification to XMPP client sessions", + module = ?MODULE, function = send_notification, + args = [{send_from, string}, {send_to, string}, {host, string}, + {unread_items, string}, {message, string}, + {type, 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, JID2, Nick2) -> + link_contacts(JID1, Nick1, JID2, Nick2, true). + +link_contacts(JID1, Nick1, JID2, Nick2, 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, JID2, Nick2, 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). + + +%%% +%%% 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_notification(SendFromUsername, SendToUsername, Host, UnreadItemsInteger, MessageBody, Type) -> + case get_resources(SendToUsername, Host) of + 404 -> + -1; + [] -> + -2; + [A|_] when is_list(A) -> + send_notification_really(SendFromUsername, SendToUsername, Host, UnreadItemsInteger, MessageBody, Type), + 0 + end. + +send_notification_really(SendFromUsername, SendToUsername, Host, UnreadItemsInteger, MessageBody, Type) -> + FromString = Host ++ "/voicemail-notifier", + ToString = SendToUsername ++ "@" ++ Host, + + XAttrs = [{"type", Type}, + {"send_from", SendFromUsername}, + {"unread_items", UnreadItemsInteger}], + XChildren = [{xmlelement, "text", [], [{xmlcdata, MessageBody}]}], + XEl = {xmlelement, "x", XAttrs, XChildren}, + + Attrs = [{"from", FromString}, {"to", ToString}, {"type", "chat"}], + Children = [XEl], + Stanza = {xmlelement, "message", Attrs, Children}, + + From = jlib:string_to_jid(FromString), + To = jlib:string_to_jid(ToString), + ejabberd_router:route(From, To, Stanza). + +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), + 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 + 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 + 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, JID2, Nick2, Push) -> + link_contacts2(JID1, Nick1, [], JID2, Nick2, [], Push). + +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), + + %% TODO: Problem: We assume that both user are local. More test + %% are needed to check if the JID is remote or not: + + %% TODO: We need to probe user2 especially, if it is not local. + %% As a quick fix, I do not go for the probe solution however, because all users + %% are local + case Subscription of + "to" -> %% Probe second user to route his presence to modified user + %% TODO: For now we assume both user are local so we do not, but we need to move to probe. + set_roster(User, Server, TJID, Nick, Subscription, Groups); + "from" -> + %% Send roster updates + set_roster(User, Server, TJID, Nick, Subscription, Groups); + "both" -> + %% Update both presence + set_roster(User, Server, TJID, Nick, Subscription, Groups), + UJID = jlib:make_jid(User, Server, ""), + set_roster(TU, TS, UJID, Nick, Subscription, Groups); + _ -> + %% Remove subscription + set_roster(User, Server, TJID, Nick, "none", Groups) + end. + + +set_roster(User, Server, TJID, Nick, Subscription, Groups) -> + GroupsXML = [{xmlelement, "group", [], [{xmlcdata, GroupString}]} || GroupString <- Groups], + Item = case Nick of + "" -> [{"jid", jlib:jid_to_string(TJID)}, {"subscription", Subscription}]; + _ -> [{"jid", jlib:jid_to_string(TJID)}, {"name", Nick}, {"subscription", Subscription}] + end, + Result = jlib:iq_to_xml(#iq{type = set, xmlns = ?NS_ROSTER, id = "push", + sub_el = [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}], + [{xmlelement, "item", Item, GroupsXML}]}]}), + lists:foreach(fun(Session) -> + JID = jlib:make_jid(Session#session.usr), + ejabberd_router:route(JID, JID, Result), + PID = element(2, Session#session.sid), + ejabberd_c2s:add_rosteritem(PID, TJID, list_to_atom(Subscription)) %% TODO: Better error management + end, get_sessions(User, Server)). + + +roster_backend(Server) -> + case lists:member(mod_roster, gen_mod:loaded_modules(Server)) of + true -> mnesia; + _ -> odbc % we assume that + 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]), + extract_group(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. + +%% ----------------------------- +%% 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) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + case catch mnesia:dirty_index_read(session, {LUser, LServer}, #session.us) of + {'EXIT', _Reason} -> []; + [] -> []; + 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 +%%% Description : +%%% Created : 12 Sep 2008 by Christophe Romain +%%% +%%% 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..97b374b6b --- /dev/null +++ b/src/mod_autofilter.erl @@ -0,0 +1,127 @@ +%%% ==================================================================== +%%% This software is copyright 2006-2010, ProcessOne. +%%% +%%% mod_autofilter +%%% +%%% @copyright 2006-2010 ProcessOne +%%% @author Christophe Romain +%%% [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, Packet}) -> + {User, Server, _} = jlib:jid_tolower(From), + case mnesia:dirty_read({autofilter, {User, Server}}) of + [#autofilter{drop=true}] -> drop; + _ -> {From, To, Packet} + end. + +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..6bd2e6e76 --- /dev/null +++ b/src/mod_c2s_debug.erl @@ -0,0 +1,175 @@ +%% Usage: +%% In config file: +%% {mod_c2s_debug, [{logdir, "/tmp/xmpplogs"}]}, +%% 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}). +-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/"), + make_dir_rec(Logdir), + {ok, #modstate{host = MyHost, logdir = Logdir}}. + +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). + +handle_call({debug_start, ClientInfo}, _From, #modstate{pid=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}}; +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 d9f4f30c8..5e6333e14 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -53,7 +53,7 @@ ]). %% hook handlers --export([user_send_packet/3]). +-export([user_send_packet/4]). -include("ejabberd.hrl"). -include("jlib.hrl"). @@ -98,11 +98,12 @@ get_features(#caps{node = Node, version = Version, exts = Exts}) -> SubNodes = [Version | Exts], lists:foldl( fun(SubNode, Acc) -> - case mnesia:dirty_read({caps_features, - node_to_binary(Node, SubNode)}) of - [] -> + BinaryNode = node_to_binary(Node, SubNode), + case cache_tab:lookup(caps_features, BinaryNode, + caps_read_fun(BinaryNode)) of + error -> Acc; - [#caps_features{features = Features}] -> + {ok, Features} -> binary_to_features(Features) ++ Acc end end, [], SubNodes). @@ -141,7 +142,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), @@ -155,7 +157,7 @@ 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. caps_stream_features(Acc, MyHost) -> @@ -196,12 +198,23 @@ disco_info(Acc, _Host, _Module, _Node, _Lang) -> %%==================================================================== %% gen_server callbacks %%==================================================================== -init([Host, _Opts]) -> +init([Host, Opts]) -> + case catch mnesia:table_info(caps_features, storage_type) of + {'EXIT', _} -> + ok; + disc_only_copies -> + ok; + _ -> + mnesia:delete_table(caps_features) + end, mnesia:create_table(caps_features, - [{disc_copies, [node()]}, + [{disc_only_copies, [node()]}, {local_content, true}, {attributes, record_info(fields, caps_features)}]), - mnesia:add_table_copy(caps_features, node(), disc_copies), + mnesia:add_table_copy(caps_features, node(), disc_only_copies), + MaxSize = gen_mod:get_opt(cache_size, Opts, 1000), + LifeTime = gen_mod:get_opt(cache_life_time, Opts, timer:hours(24) div 1000), + cache_tab:new(caps_features, [{max_size, MaxSize}, {life_time, LifeTime}]), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 75), ejabberd_hooks:add(c2s_stream_features, Host, @@ -252,8 +265,9 @@ code_change(_OldVsn, State, _Extra) -> feature_request(Host, From, Caps, [SubNode | Tail] = SubNodes) -> Node = Caps#caps.node, BinaryNode = node_to_binary(Node, SubNode), - case mnesia:dirty_read({caps_features, BinaryNode}) of - [] -> + case cache_tab:lookup(caps_features, BinaryNode, + caps_read_fun(BinaryNode)) of + error -> IQ = #iq{type = get, xmlns = ?NS_DISCO_INFO, sub_el = [{xmlelement, "query", @@ -284,11 +298,13 @@ feature_response(#iq{type = result, (_) -> [] end, Els), - mnesia:dirty_write( - #caps_features{node_pair = BinaryNode, - features = features_to_binary(Features)}); + BinaryFeatures = features_to_binary(Features), + cache_tab:insert( + caps_features, BinaryNode, BinaryFeatures, + caps_write_fun(BinaryNode, BinaryFeatures)); false -> - mnesia:dirty_write(#caps_features{node_pair = BinaryNode}) + cache_tab:insert(caps_features, BinaryNode, [], + caps_write_fun(BinaryNode, [])) end, feature_request(Host, From, Caps, SubNodes); feature_response(timeout, _Host, _From, _Caps, _SubNodes) -> @@ -297,7 +313,8 @@ feature_response(_IQResult, Host, From, Caps, [SubNode | SubNodes]) -> %% We got type=error or invalid type=result stanza, so %% we cache empty feature not to probe the client permanently BinaryNode = node_to_binary(Caps#caps.node, SubNode), - mnesia:dirty_write(#caps_features{node_pair = BinaryNode}), + cache_tab:insert(caps_features, BinaryNode, [], + caps_write_fun(BinaryNode, [])), feature_request(Host, From, Caps, SubNodes). node_to_binary(Node, SubNode) -> @@ -306,6 +323,23 @@ node_to_binary(Node, SubNode) -> features_to_binary(L) -> [list_to_binary(I) || I <- L]. binary_to_features(L) -> [binary_to_list(I) || I <- L]. +caps_read_fun(Node) -> + fun() -> + case mnesia:dirty_read({caps_features, Node}) of + [#caps_features{features = Features}] -> + {ok, Features}; + _ -> + error + end + end. + +caps_write_fun(Node, Features) -> + fun() -> + mnesia:dirty_write( + #caps_features{node_pair = Node, + features = Features}) + end. + make_my_disco_hash(Host) -> JID = jlib:make_jid("", Host, ""), case {ejabberd_hooks:run_fold(disco_local_features, 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 +%%% [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 c9102f2c0..16b70deaa 100644 --- a/src/mod_ip_blacklist.erl +++ b/src/mod_ip_blacklist.erl @@ -85,7 +85,7 @@ loop(_State) -> %% TODO: Support comment lines starting by % update_bl_c2s() -> ?INFO_MSG("Updating C2S Blacklist", []), - case http: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_irc/mod_irc_connection.erl b/src/mod_irc/mod_irc_connection.erl index 107e6602e..39a06a294 100644 --- a/src/mod_irc/mod_irc_connection.erl +++ b/src/mod_irc/mod_irc_connection.erl @@ -105,8 +105,16 @@ init([From, Host, Server, Username, Encoding, Port, Password]) -> open_socket(init, StateData) -> Addr = StateData#state.server, Port = StateData#state.port, - ?DEBUG("connecting to ~s:~p~n", [Addr, Port]), - case gen_tcp:connect(Addr, Port, [binary, {packet, 0}]) of + ?DEBUG("Connecting with IPv6 to ~s:~p", [Addr, Port]), + Connect6 = gen_tcp:connect(Addr, Port, [inet6, binary, {packet, 0}]), + Connect = case Connect6 of + {error, _} -> + ?DEBUG("Connection with IPv6 to ~s:~p failed. Now using IPv4.", [Addr, Port]), + gen_tcp:connect(Addr, Port, [inet, binary, {packet, 0}]); + _ -> + Connect6 + end, + case Connect of {ok, Socket} -> NewStateData = StateData#state{socket = Socket}, if 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 f79f4cd24..c2af1ff83 100644 --- a/src/mod_muc/mod_muc.erl +++ b/src/mod_muc/mod_muc.erl @@ -41,6 +41,9 @@ create_room/5, process_iq_disco_items/4, broadcast_service_message/2, + register_room/3, + migrate/1, + get_vh_rooms/1, can_use_nick/3]). %% gen_server callbacks @@ -109,7 +112,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 = ejabberd_cluster:get_node({Name, RoomHost}), + gen_server:call({Proc, Node}, {create, Name, From, Nick, Opts}). store_room(Host, Name, Opts) -> F = fun() -> @@ -162,6 +167,22 @@ can_use_nick(Host, JID, Nick) -> U == LUS end. +migrate(After) -> + Rs = mnesia:dirty_select( + muc_online_room, + [{#muc_online_room{name_host = '$1', pid = '$2', _ = '_'}, + [], + ['$$']}]), + lists:foreach( + fun([NameHost, Pid]) -> + case ejabberd_cluster:get_node_new(NameHost) of + Node when Node /= node() -> + mod_muc_room:migrate(Pid, Node, After); + _ -> + ok + end + end, Rs). + %%==================================================================== %% gen_server callbacks %%==================================================================== @@ -174,6 +195,7 @@ can_use_nick(Host, JID, Nick) -> %% Description: Initiates the server %%-------------------------------------------------------------------- init([Host, Opts]) -> + update_muc_online_table(), mnesia:create_table(muc_room, [{disc_copies, [node()]}, {attributes, record_info(fields, muc_room)}]), @@ -182,14 +204,13 @@ init([Host, Opts]) -> {attributes, record_info(fields, muc_registered)}]), 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}]), MyHost = gen_mod:get_opt_host(Host, Opts, "conference.@HOST@"), update_tables(MyHost), - clean_table_from_bad_node(node(), MyHost), mnesia:add_table_index(muc_registered, nick), - 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), @@ -198,6 +219,7 @@ init([Host, Opts]) -> 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_hash_update, ?MODULE, migrate, 100), load_permanent_rooms(MyHost, Host, {Access, AccessCreate, AccessAdmin, AccessPersistent}, HistorySize, @@ -264,12 +286,19 @@ handle_info({route, From, To, Packet}, default_room_opts = DefRoomOpts, history_size = HistorySize, 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 ejabberd_cluster:get_node({U, S}) of + Node when Node == node() -> + case catch do_route(Host, ServerHost, Access, HistorySize, + 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) -> @@ -277,10 +306,7 @@ 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:sync_dirty(F), {noreply, State}; handle_info(_Info, State) -> {noreply, State}. @@ -293,6 +319,7 @@ handle_info(_Info, State) -> %% The return value is ignored. %%-------------------------------------------------------------------- terminate(_Reason, State) -> + ejabberd_hooks:delete(node_hash_update, ?MODULE, migrate, 100), ejabberd_router:unregister_route(State#state.host), ok. @@ -532,17 +559,22 @@ load_permanent_rooms(Host, ServerHost, Access, HistorySize, 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 ejabberd_cluster:get_node({Room, Host}) of + Node when Node == node() -> + 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); + _ -> + ok + end; _ -> ok end @@ -568,11 +600,10 @@ start_new_room(Host, ServerHost, Access, Room, 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:sync_dirty(F). iq_disco_info(Lang) -> [{xmlelement, "identity", @@ -601,7 +632,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), @@ -621,19 +652,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)); @@ -656,6 +677,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() @@ -813,7 +855,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, [], ejabberd_cluster:get_nodes()), + lists:ukeysort(#muc_online_room.name_host, Rooms). get_vh_rooms(Host) -> mnesia:dirty_select(muc_online_room, @@ -821,39 +878,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 diff --git a/src/mod_muc/mod_muc_log.erl b/src/mod_muc/mod_muc_log.erl index d010e86b8..fbc2d5cbe 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). @@ -950,7 +950,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 0eeea91e8..e5b528a04 100644 --- a/src/mod_muc/mod_muc_room.erl +++ b/src/mod_muc/mod_muc_room.erl @@ -27,14 +27,19 @@ -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_link/2, start/9, start/7, + start/2, + migrate/3, route/4]). %% gen_fsm callbacks @@ -44,6 +49,7 @@ handle_sync_event/4, handle_info/3, terminate/3, + print_state/1, code_change/4]). -include("ejabberd.hrl"). @@ -63,16 +69,12 @@ %% 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. %%%---------------------------------------------------------------------- @@ -80,7 +82,8 @@ %%%---------------------------------------------------------------------- start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts) -> - ?SUPERVISOR_START. + ?SUPERVISOR_START([Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Creator, Nick, DefRoomOpts]). start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup), @@ -88,16 +91,26 @@ start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]). +start(StateName, StateData) -> + ServerHost = StateData#state.server_host, + ?SUPERVISOR_START([StateName, StateData]). + start_link(Host, ServerHost, Access, Room, HistorySize, 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, + RoomShaper, Creator, Nick, DefRoomOpts], + ?FSMOPTS). start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> - gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, - RoomShaper, Opts], - ?FSMOPTS). + ?GEN_FSM:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Opts], + ?FSMOPTS). + +start_link(StateName, StateData) -> + ?GEN_FSM:start_link(?MODULE, [StateName, StateData], ?FSMOPTS). + +migrate(FsmRef, Node, After) -> + ?GEN_FSM:send_all_state_event(FsmRef, {migrate, Node, After}). %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm @@ -139,7 +152,11 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) -> 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 @@ -580,6 +597,9 @@ handle_event(destroy, StateName, StateData) -> handle_event({set_affiliations, Affiliations}, StateName, StateData) -> {next_state, StateName, StateData#state{affiliations = Affiliations}}; +handle_event({migrate, Node, After}, StateName, StateData) when Node /= node() -> + {migrate, StateData, + {Node, ?MODULE, start, [StateName, StateData]}, After * 2}; handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. @@ -612,6 +632,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} | @@ -701,6 +724,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]), @@ -739,7 +769,7 @@ terminate(Reason, _StateName, StateData) -> %%%---------------------------------------------------------------------- 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) -> @@ -1625,13 +1655,12 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> From, Err), StateData; captcha_required -> - ID = randoms:get_string(), SID = xml:get_attr_s("id", Attrs), RoomJID = StateData#state.jid, To = jlib:jid_replace_resource(RoomJID, Nick), case ejabberd_captcha:create_captcha( - ID, SID, RoomJID, To, Lang, From) of - {ok, CaptchaEls} -> + SID, RoomJID, To, Lang, From) of + {ok, ID, CaptchaEls} -> MsgPkt = {xmlelement, "message", [{"id", ID}], CaptchaEls}, Robots = ?DICT:store(From, {Nick, Packet}, StateData#state.robots), diff --git a/src/mod_offline.erl b/src/mod_offline.erl index 88d0572de..2448a17c0 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -30,16 +30,14 @@ -behaviour(gen_mod). -export([start/2, - loop/1, + init/1, stop/1, store_packet/3, resend_offline_messages/2, pop_offline_messages/3, - get_sm_features/5, remove_expired_messages/0, remove_old_messages/1, remove_user/2, - get_queue_length/2, webadmin_page/3, webadmin_user/4, webadmin_user_parse_query/5, @@ -55,9 +53,6 @@ -define(PROCNAME, ejabberd_offline). -define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000). -%% default value for the maximum number of user messages --define(MAX_USER_MESSAGES, infinity). - start(Host, Opts) -> mnesia:create_table(offline_msg, [{disc_only_copies, [node()]}, @@ -72,10 +67,6 @@ 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, @@ -83,19 +74,23 @@ start(Host, Opts) -> 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), + ?MODULE, count_offline_messages, 50), + MaxOfflineMsgs = gen_mod:get_opt(user_max_messages, Opts, infinity), register(gen_mod:get_module_proc(Host, ?PROCNAME), - spawn(?MODULE, loop, [AccessMaxOfflineMsgs])). + spawn(?MODULE, init, [MaxOfflineMsgs])). -loop(AccessMaxOfflineMsgs) -> +%% MaxOfflineMsgs is either infinity of integer > 0 +init(infinity) -> + loop(infinity); +init(MaxOfflineMsgs) + when is_integer(MaxOfflineMsgs), MaxOfflineMsgs > 0 -> + loop(MaxOfflineMsgs). + +loop(MaxOfflineMsgs) -> receive #offline_msg{us=US} = Msg -> Msgs = receive_all(US, [Msg]), Len = length(Msgs), - {User, Host} = US, - MaxOfflineMsgs = get_max_user_messages(AccessMaxOfflineMsgs, - User, Host), F = fun() -> %% Only count messages if needed: Count = if MaxOfflineMsgs =/= infinity -> @@ -121,18 +116,9 @@ loop(AccessMaxOfflineMsgs) -> end end, mnesia:transaction(F), - loop(AccessMaxOfflineMsgs); + loop(MaxOfflineMsgs); _ -> - loop(AccessMaxOfflineMsgs) - end. - -%% Function copied from ejabberd_sm.erl: -get_max_user_messages(AccessRule, LUser, Host) -> - case acl:match_rule( - Host, AccessRule, jlib:make_jid(LUser, Host, "")) of - Max when is_integer(Max) -> Max; - infinity -> infinity; - _ -> ?MAX_USER_MESSAGES + loop(MaxOfflineMsgs) end. receive_all(US, Msgs) -> @@ -153,8 +139,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, @@ -165,27 +149,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(), @@ -206,22 +175,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, _} -> + case find_x_event(Els) of + false -> true; - %% There a chatstates subelement and other stuff, but no x:event - {false, CEl, true} when CEl /= 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 @@ -249,19 +208,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(_, []) -> @@ -310,13 +266,6 @@ resend_offline_messages(User, Server) -> {xmlelement, Name, Attrs, 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, @@ -346,14 +295,7 @@ pop_offline_messages(Ls, User, Server) -> {xmlelement, Name, Attrs, 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( + calendar:now_to_universal_time( R#offline_msg.timestamp))]}} end, lists:filter( @@ -370,7 +312,6 @@ pop_offline_messages(Ls, User, Server) -> Ls end. - remove_expired_messages() -> TimeStamp = now(), F = fun() -> @@ -533,9 +474,8 @@ webadmin_page(Acc, _, _) -> Acc. user_queue(User, Server, Query, Lang) -> US = {jlib:nodeprep(User), jlib:nameprep(Server)}, Res = user_queue_parse_query(US, Query), - MsgsAll = lists:keysort(#offline_msg.timestamp, - mnesia:dirty_read({offline_msg, US})), - Msgs = get_messages_subset(User, Server, MsgsAll), + Msgs = lists:keysort(#offline_msg.timestamp, + mnesia:dirty_read({offline_msg, US})), FMsgs = lists:map( fun(#offline_msg{timestamp = TimeStamp, from = From, to = To, @@ -617,32 +557,9 @@ user_queue_parse_query(US, Query) -> us_to_list({User, Server}) -> jlib:jid_to_string({User, Server, ""}). -get_queue_length(User, Server) -> - length(mnesia:dirty_read({offline_msg, {User, Server}})). - -get_messages_subset(User, Host, MsgsAll) -> - Access = gen_mod:get_module_opt(Host, ?MODULE, access_max_user_messages, - max_user_offline_messages), - MaxOfflineMsgs = case get_max_user_messages(Access, User, Host) of - Number when is_integer(Number) -> Number; - _ -> 100 - end, - Length = length(MsgsAll), - get_messages_subset2(MaxOfflineMsgs, Length, MsgsAll). - -get_messages_subset2(Max, Length, MsgsAll) when Length =< Max*2 -> - MsgsAll; -get_messages_subset2(Max, Length, MsgsAll) -> - FirstN = Max, - {MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll), - MsgsLastN = lists:nthtail(Length - FirstN - FirstN, Msgs2), - NoJID = jlib:make_jid("...", "...", ""), - IntermediateMsg = #offline_msg{timestamp = now(), from = NoJID, to = NoJID, - packet = {xmlelement, "...", [], []}}, - MsgsFirstN ++ [IntermediateMsg] ++ MsgsLastN. - webadmin_user(Acc, User, Server, Lang) -> - QueueLen = get_queue_length(jlib:nodeprep(User), jlib:nameprep(Server)), + US = {jlib:nodeprep(User), jlib:nameprep(Server)}, + QueueLen = length(mnesia:dirty_read({offline_msg, US})), FQueueLen = [?AC("queue/", integer_to_list(QueueLen))], Acc ++ [?XCT("h3", "Offline Messages:")] ++ FQueueLen ++ [?C(" "), ?INPUTT("submit", "removealloffline", "Remove All Offline Messages")]. diff --git a/src/mod_offline_odbc.erl b/src/mod_offline_odbc.erl index ab63b00b3..420ff581f 100644 --- a/src/mod_offline_odbc.erl +++ b/src/mod_offline_odbc.erl @@ -32,13 +32,11 @@ -export([count_offline_messages/2]). -export([start/2, - loop/2, + init/2, stop/1, store_packet/3, pop_offline_messages/3, - get_sm_features/5, remove_user/2, - get_queue_length/2, webadmin_page/3, webadmin_user/4, webadmin_user_parse_query/5, @@ -54,9 +52,6 @@ -define(PROCNAME, ejabberd_offline). -define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000). -%% default value for the maximum number of user messages --define(MAX_USER_MESSAGES, infinity). - start(Host, Opts) -> ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, store_packet, 50), @@ -66,10 +61,6 @@ 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, @@ -78,17 +69,22 @@ start(Host, Opts) -> ?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), + MaxOfflineMsgs = gen_mod:get_opt(user_max_messages, Opts, infinity), register(gen_mod:get_module_proc(Host, ?PROCNAME), - spawn(?MODULE, loop, [Host, AccessMaxOfflineMsgs])). + spawn(?MODULE, init, [Host, MaxOfflineMsgs])). -loop(Host, AccessMaxOfflineMsgs) -> +%% MaxOfflineMsgs is either infinity of integer > 0 +init(Host, infinity) -> + loop(Host, infinity); +init(Host, MaxOfflineMsgs) + when is_integer(MaxOfflineMsgs), MaxOfflineMsgs > 0 -> + loop(Host, MaxOfflineMsgs). + +loop(Host, MaxOfflineMsgs) -> receive #offline_msg{user = User} = Msg -> Msgs = receive_all(User, [Msg]), Len = length(Msgs), - MaxOfflineMsgs = get_max_user_messages(AccessMaxOfflineMsgs, - User, Host), %% Only count existing messages if needed: Count = if MaxOfflineMsgs =/= infinity -> @@ -116,17 +112,11 @@ loop(Host, AccessMaxOfflineMsgs) -> Els ++ [jlib:timestamp_to_xml( calendar:now_to_universal_time( - M#offline_msg.timestamp), - utc, - jlib:make_jid("", Host, ""), - "Offline Storage"), - %% TODO: Delete the next three lines once XEP-0091 is Obsolete - jlib:timestamp_to_xml( - calendar:now_to_universal_time( M#offline_msg.timestamp))]}, XML = ejabberd_odbc:escape( - xml:element_to_binary(Packet)), + lists:flatten( + xml:element_to_string(Packet))), odbc_queries:add_spool_sql(Username, XML) end, Msgs), case catch odbc_queries:add_spool(Host, Query) of @@ -138,18 +128,9 @@ loop(Host, AccessMaxOfflineMsgs) -> ok end end, - loop(Host, AccessMaxOfflineMsgs); + loop(Host, MaxOfflineMsgs); _ -> - loop(Host, AccessMaxOfflineMsgs) - end. - -%% Function copied from ejabberd_sm.erl: -get_max_user_messages(AccessRule, LUser, Host) -> - case acl:match_rule( - Host, AccessRule, jlib:make_jid(LUser, Host, "")) of - Max when is_integer(Max) -> Max; - infinity -> infinity; - _ -> ?MAX_USER_MESSAGES + loop(Host, MaxOfflineMsgs) end. receive_all(Username, Msgs) -> @@ -170,8 +151,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, @@ -182,27 +161,12 @@ stop(Host) -> exit(whereis(Proc), stop), ok. -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} = To, TimeStamp = now(), @@ -223,22 +187,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, _} -> + case find_x_event(Els) of + false -> true; - %% There a chatstates subelement and other stuff, but no x:event - {false, CEl, true} when CEl /= 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 @@ -260,25 +214,22 @@ check_event_chatstates(From, To, Packet) -> {xmlelement, "offline", [], []}]}] }), true - end; + end; _ -> false 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(_, []) -> @@ -378,7 +329,7 @@ user_queue(User, Server, Query, Lang) -> Username = ejabberd_odbc:escape(LUser), US = {LUser, LServer}, Res = user_queue_parse_query(Username, LServer, Query), - MsgsAll = case catch ejabberd_odbc:sql_query( + Msgs = case catch ejabberd_odbc:sql_query( LServer, ["select username, xml from spool" " where username='", Username, "'" @@ -396,7 +347,6 @@ user_queue(User, Server, Query, Lang) -> _ -> [] end, - Msgs = get_messages_subset(User, Server, MsgsAll), FMsgs = lists:map( fun({xmlelement, _Name, _Attrs, _Els} = Msg) -> @@ -483,8 +433,11 @@ user_queue_parse_query(Username, LServer, Query) -> us_to_list({User, Server}) -> jlib:jid_to_string({User, Server, ""}). -get_queue_length(Username, LServer) -> - case catch ejabberd_odbc:sql_query( +webadmin_user(Acc, User, Server, Lang) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + Username = ejabberd_odbc:escape(LUser), + QueueLen = case catch ejabberd_odbc:sql_query( LServer, ["select count(*) from spool" " where username='", Username, "';"]) of @@ -492,32 +445,7 @@ get_queue_length(Username, LServer) -> SCount; _ -> 0 - end. - -get_messages_subset(User, Host, MsgsAll) -> - Access = gen_mod:get_module_opt(Host, ?MODULE, access_max_user_messages, - max_user_offline_messages), - MaxOfflineMsgs = case get_max_user_messages(Access, User, Host) of - Number when is_integer(Number) -> Number; - _ -> 100 - end, - Length = length(MsgsAll), - get_messages_subset2(MaxOfflineMsgs, Length, MsgsAll). - -get_messages_subset2(Max, Length, MsgsAll) when Length =< Max*2 -> - MsgsAll; -get_messages_subset2(Max, Length, MsgsAll) -> - FirstN = Max, - {MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll), - MsgsLastN = lists:nthtail(Length - FirstN - FirstN, Msgs2), - IntermediateMsg = {xmlelement, "...", [], []}, - MsgsFirstN ++ [IntermediateMsg] ++ MsgsLastN. - -webadmin_user(Acc, User, Server, Lang) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - Username = ejabberd_odbc:escape(LUser), - QueueLen = get_queue_length(Username, LServer), + end, FQueueLen = [?AC("queue/", QueueLen)], Acc ++ [?XCT("h3", "Offline Messages:")] ++ FQueueLen ++ [?C(" "), ?INPUTT("submit", "removealloffline", "Remove All Offline Messages")]. diff --git a/src/mod_ping.erl b/src/mod_ping.erl index 80b6dac72..a650f983e 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, @@ -107,6 +107,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); _ -> @@ -121,6 +123,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, @@ -193,7 +197,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.erl b/src/mod_privacy.erl index 0c9d089e0..d25bc9b7b 100644 --- a/src/mod_privacy.erl +++ b/src/mod_privacy.erl @@ -392,12 +392,13 @@ process_list_set(LUser, LServer, {value, Name}, Els) -> {atomic, {error, _} = Error} -> Error; {atomic, {result, _} = Res} -> + NeedDb = is_list_needdb(List), ejabberd_router:route( jlib:make_jid(LUser, LServer, ""), jlib:make_jid(LUser, LServer, ""), {xmlelement, "broadcast", [], [{privacy_list, - #userlist{name = Name, list = List}, + #userlist{name = Name, list = List, needdb = NeedDb}, Name}]}), Res; _ -> diff --git a/src/mod_privacy.hrl b/src/mod_privacy.hrl index 0e0b02b21..3f4489b10 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_privacy_odbc.erl b/src/mod_privacy_odbc.erl index 5ad2fb958..593e99140 100644 --- a/src/mod_privacy_odbc.erl +++ b/src/mod_privacy_odbc.erl @@ -392,12 +392,13 @@ process_list_set(LUser, LServer, {value, Name}, Els) -> {atomic, {error, _} = Error} -> Error; {atomic, {result, _} = Res} -> + NeedDb = is_list_needdb(List), ejabberd_router:route( jlib:make_jid(LUser, LServer, ""), jlib:make_jid(LUser, LServer, ""), {xmlelement, "broadcast", [], [{privacy_list, - #userlist{name = Name, list = List}, + #userlist{name = Name, list = List, needdb = NeedDb}, Name}]}), Res; _ -> diff --git a/src/mod_proxy65/mod_proxy65_sm.erl b/src/mod_proxy65/mod_proxy65_sm.erl index 569458f6a..bdd1b5bb8 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 a9293212d..78a3b3f6d 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, @@ -766,13 +767,15 @@ out_subscription(User, Server, JID, subscribed) -> [] -> user_resources(PUser, PServer); _ -> [PResource] end, - presence(Server, {presence, PUser, PServer, PResources, Owner}); + presence(Server, {presence, PUser, PServer, PResources, Owner}), + true; out_subscription(_,_,_,_) -> - ok. + true. in_subscription(_, User, Server, Owner, unsubscribed, _) -> - unsubscribe_user(jlib:make_jid(User, Server, ""), Owner); + unsubscribe_user(jlib:make_jid(User, Server, ""), Owner), + true; in_subscription(_, _, _, _, _, _) -> - ok. + true. unsubscribe_user(Entity, Owner) -> BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), diff --git a/src/mod_pubsub/node_flat_odbc.erl b/src/mod_pubsub/node_flat_odbc.erl index a255e9b0d..95998a554 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_pep_odbc.erl b/src/mod_pubsub/node_pep_odbc.erl index 9f0071bbc..5d2fb0e1e 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_clean.erl b/src/mod_pubsub/pubsub_clean.erl new file mode 100644 index 000000000..814cee0fb --- /dev/null +++ b/src/mod_pubsub/pubsub_clean.erl @@ -0,0 +1,38 @@ +-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) -> + 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..97ca8275f --- /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_match_object(#pubsub_state{stateid={'_', NodeId}, _='_'}). +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_match_object(#pubsub_item{itemid={'_', NodeId}, _='_'}). +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 <- mnesia:dirty_match_object(#pubsub_state{stateid={'_', NodeId}, affiliation=owner, _='_'})]. + +orphan_items(NodeId) -> + itemids(NodeId) -- lists:foldl(fun(S, A) -> A++S#pubsub_state.items end, [], mnesia:dirty_match_object(#pubsub_state{stateid={'_', 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_service_log.erl b/src/mod_service_log.erl index 8d33ac4d8..44fcfd25c 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_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 +%%% [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_time.erl b/src/mod_time.erl index a8a43c0ab..e96c0ea72 100644 --- a/src/mod_time.erl +++ b/src/mod_time.erl @@ -76,7 +76,13 @@ process_local_iq(_From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> {UTC, UTC_diff} = jlib:timestamp_to_iso(Now_universal, utc), Seconds_diff = calendar:datetime_to_gregorian_seconds(Now_local) - calendar:datetime_to_gregorian_seconds(Now_universal), - {Hd, Md, _} = calendar:seconds_to_time(Seconds_diff), + {Hd, Md, _} = case Seconds_diff >= 0 of + true -> + calendar:seconds_to_time(Seconds_diff); + false -> + {Hd0, Md0, Sd0} = calendar:seconds_to_time(-Seconds_diff), + {-Hd0, Md0, Sd0} + end, {_, TZO_diff} = jlib:timestamp_to_iso({{0, 0, 0}, {0, 0, 0}}, {Hd, Md}), IQ#iq{type = result, sub_el = [{xmlelement, "time", diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index ed52a0593..53f8b58dc 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -169,7 +169,7 @@ start_link(Host, Opts) -> init([Host, Opts]) -> State = parse_options(Host, Opts), - IQDisc = gen_mod:get_opt(iqdisc, Opts, parallel), + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, ?MODULE, process_local_iq, IQDisc), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_VCARD, diff --git a/src/mod_xmlrpc.erl b/src/mod_xmlrpc.erl new file mode 100644 index 000000000..d01838a0d --- /dev/null +++ b/src/mod_xmlrpc.erl @@ -0,0 +1,876 @@ +%%%---------------------------------------------------------------------- +%%% 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]). + +-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}]}) -> + [{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, JID2, Nick2) 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; + +%% get_roster struct[{user, String}, {server, String}] +%% array[struct[{jid, String}, {group, 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 -> + {error, no_roster} + 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 -> + {error, no_roster} + 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. + +roster_push(User, Server, JID, Nick, Subscription) -> + 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}], []}, + Item = case Nick of + "" -> [{"jid", JID}, {"subscription", Subscription}]; + _ -> [{"jid", JID}, {"name", Nick}, {"subscription", Subscription}] + end, + Result = jlib:iq_to_xml(#iq{type = set, xmlns = ?NS_ROSTER, id = "push", + sub_el = [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}], + [{xmlelement, "item", Item, []}]}]}), + 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), Subscription =/= "remove"]). + +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])}, + {group, extract_group(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. + +%% ----------------------------- +%% 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) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + case catch mnesia:dirty_index_read(session, {LUser, LServer}, #session.us) of + {'EXIT', _Reason} -> []; + [] -> []; + 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 4a6481525..dc23dd928 100644 --- a/src/odbc/ejabberd_odbc.erl +++ b/src/odbc/ejabberd_odbc.erl @@ -40,7 +40,8 @@ escape/1, escape_like/1, to_bool/1, - keep_alive/1]). + keep_alive/1, + sql_query_on_all_connections/2]). %% gen_fsm callbacks -export([init/1, @@ -98,6 +99,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) -> @@ -427,13 +435,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, @@ -507,6 +517,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/p1_fsm.erl b/src/p1_fsm.erl index 9ca924112..bc47e9c67 100644 --- a/src/p1_fsm.erl +++ b/src/p1_fsm.erl @@ -555,7 +555,17 @@ 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 + 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 + {badrpc, timeout} -> + normal; {badrpc, _} = Err -> {migration_error, Err}; {'EXIT', _} = Err -> @@ -565,7 +575,9 @@ handle_msg(Msg, Parent, Name, StateName, StateData, Mod, _Time, {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}} @@ -608,7 +620,17 @@ 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 + 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 + {badrpc, timeout} -> + normal; {badrpc, R} -> {migration_error, R}; {'EXIT', R} -> @@ -618,7 +640,9 @@ handle_msg(Msg, Parent, Name, StateName, StateData, {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}} diff --git a/src/web/ejabberd_http.erl b/src/web/ejabberd_http.erl index 2f2315017..f4ee13e20 100644 --- a/src/web/ejabberd_http.erl +++ b/src/web/ejabberd_http.erl @@ -65,7 +65,8 @@ request_tp, request_headers = [], end_of_request = false, - trail = "" + trail = "", + websocket_handlers = [] }). @@ -134,11 +135,16 @@ 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]), ?INFO_MSG("started: ~p", [{SockMod1, Socket1}]), State = #state{sockmod = SockMod1, socket = Socket1, - request_handlers = RequestHandlers}, + request_handlers = RequestHandlers, + websocket_handlers = WebSocketHandlers}, receive_headers(State). @@ -148,6 +154,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; @@ -289,6 +298,11 @@ 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). %% @spec (SockMod, HostPort) -> {Host::string(), Port::integer(), TP} %% where %% SockMod = gen_tcp | tls @@ -313,8 +327,38 @@ 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 -> + ?DEBUG("~p matches ~p", [Ws#ws.path, HandlerPathPrefix]), + 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, + WS2 = Ws#ws{local_path = LocalPath, + protocol=Protocol, + acceptable_origins=Origins}, + case ejabberd_websocket:is_acceptable(WS2) of + true -> + ejabberd_websocket:connect(WS2, HandlerModule); + false -> + process(HandlersLeft, Ws) + end; + false -> + ?DEBUG("HandlersLeft : ~p ", [HandlersLeft]), + process(HandlersLeft, Ws) + end; process(Handlers, Request) -> [{HandlerPathPrefix, HandlerModule} | HandlersLeft] = Handlers, @@ -344,6 +388,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 @@ -364,31 +409,51 @@ process_request(#state{request_method = Method, _ -> SockMod:peername(Socket) end, - 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} = lists:keyfind("Origin", 1, RequestHeaders), + Ws = #ws{socket = Socket, + sockmod = SockMod, + ws_autoexit = true, + ip = IP, + path = LPath, + vsn = VSN, + host = Host, + port = Port, + origin = Origin, + headers = RequestHeaders + }, + process(WebSocketHandlers, Ws), + ?DEBUG("It is a websocket.",[]), + 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) + end + end end; process_request(#state{request_method = Method, diff --git a/src/web/ejabberd_http.hrl b/src/web/ejabberd_http.hrl index 8bb96220a..89c2a55c4 100644 --- a/src/web/ejabberd_http.hrl +++ b/src/web/ejabberd_http.hrl @@ -32,3 +32,21 @@ 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, + protocol, + acceptable_origins + }). \ No newline at end of file diff --git a/src/web/ejabberd_http_bind.erl b/src/web/ejabberd_http_bind.erl index 27ea3c089..98f8ea520 100644 --- a/src/web/ejabberd_http_bind.erl +++ b/src/web/ejabberd_http_bind.erl @@ -27,6 +27,7 @@ setopts/2, controlling_process/2, become_controller/2, + change_controller/2, custom_receiver/1, reset_stream/1, change_shaper/2, @@ -121,9 +122,19 @@ 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}; - _ -> check_bind_module(XMPPDomain), - {error, "Cannot start HTTP bind session"} + {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) -> @@ -140,7 +151,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) -> @@ -152,6 +174,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. @@ -170,7 +195,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(), @@ -192,12 +216,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, "BOSH module not started"}; + "xmlns='" ++ ?NS_HTTP_BIND ++ "'>Internal Server Error"}; {ok, Pid} -> handle_session_start( Pid, XmppDomain, Sid, Rid, Attrs, @@ -223,10 +247,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, " mnesia:write( #http_bind{id = Sid, @@ -340,6 +364,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{ @@ -404,6 +429,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, @@ -537,6 +570,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}. @@ -804,10 +840,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 -> @@ -1225,7 +1261,36 @@ check_default_xmlns({xmlelement, Name, Attrs, Els} = El) -> %% 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 -> ok; + 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.",[]) + "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 0bdbc6287..f7550b099 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}) @@ -272,7 +275,13 @@ handle_event(_Event, StateName, StateData) -> %% {stop, Reason, Reply, NewStateData} %%---------------------------------------------------------------------- handle_sync_event({send, Packet}, _From, StateName, StateData) -> - Output = StateData#state.output ++ [lists:flatten(Packet)], + Packet2 = if + is_binary(Packet) -> + binary_to_list(Packet); + true -> + Packet + end, + Output = StateData#state.output ++ [lists:flatten(Packet2)], Reply = ok, {reply, Reply, StateName, StateData#state{output = Output}}; @@ -350,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), @@ -375,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. @@ -446,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..f8ffb3aef --- /dev/null +++ b/src/web/ejabberd_http_ws.erl @@ -0,0 +1,202 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_websocket.erl +%%% Author : Eric Cestari +%%% Purpose : XMPP Websocket support +%%% Created : 09-10-2010 by Eric Cestari +%%% +%%% +%%% 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) -> + Reply = ok, + {stop, normal, Reply, StateData}. + +handle_info({browser, Packet}, StateName, StateData)-> + NewState = case StateData#state.waiting_input of + false -> + Input = [StateData#state.input|Packet], + StateData#state{input = Input}; + {Receiver, _Tag} -> + 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..2b4d41292 --- /dev/null +++ b/src/web/ejabberd_websocket.erl @@ -0,0 +1,275 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_websocket.erl +%%% Author : Eric Cestari +%%% Purpose : XMPP Websocket support +%%% Created : 09-10-2010 by Eric Cestari +%%% +%%% 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 , Joe Armstrong. +%%% All rights reserved. +%%% +%%% Code portions from Joe Armstrong have been originally taken under MIT license at the address: +%%% +%%% +%%% 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-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})-> + ClientProtocol = lists:keyfind("Sec-WebSocket-Protocol",1, Headers), + case {(Origin == []) or lists:member(Origin, Origins), ClientProtocol, Protocol } of + {false, _, _} -> + ?DEBUG("client does not come from authorized origin", []), + false; + {_, false, _} -> + ?DEBUG("Client did not ask for protocol", []), + true; + {_, {_, P}, P} -> + ?DEBUG("Protocoles are matching", []), + true; + _ -> false + end. + + +% Connect and handshake with Websocket. +connect(#ws{vsn = Vsn, socket = Socket, 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, Origin, Host, Port}), + % send handshake back + %?DEBUG("building handshake response : ~p", [HandshakeServer]), + SockMod:send(Socket, 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(Socket, none, WsHandleLoopPid, SockMod, WsAutoExit). + + +check_websockets([], _Headers) -> false; +check_websockets([Vsn|T], Headers) -> + case check_websocket(Vsn, Headers) of + false -> check_websockets(T, Headers); + {true, Vsn} -> {true, Vsn} + 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(_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 + {Tag, 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, Origin, Host, Port}) -> + % build data + {_, Key1} = lists:keyfind("Sec-Websocket-Key1",1, Headers), + {_, Key2} = lists:keyfind("Sec-Websocket-Key2",1, Headers), + % 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, + %?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://", + string:join([Host, integer_to_list(Port)],":"), + "/",string:join(Path,"/") , "\r\n\r\n", + build_challenge({'draft-hixie', 0}, {Key1, Key2, Body}) + ]; +handshake({'draft-hixie', 68}, _Sock,_SocketMod, _Headers, {Path, Origin, Host, Port}) -> + % prepare handhsake response + ["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://", + lists:concat([Host, integer_to_list(Port)]), + "/",string:join(Path,"/"), "\r\n\r\n" + ]. + +% Function: List +% Description: Builds the challenge for a handshake response. +% Code portions from Sergio Veiga +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 = <>, + erlang:md5(Ckey). + + +ws_loop(Socket, Buffer, WsHandleLoopPid, SocketMode, WsAutoExit) -> + receive + {tcp, Socket, Data} -> + handle_data(Buffer, Data, 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, iolist_to_binary([0,Data,255])), + ws_loop(Socket, Buffer, 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(Socket, Buffer, WsHandleLoopPid, SocketMode, WsAutoExit) + end. + +% Buffering and data handling +handle_data(none,<<0,T/binary>>, Socket, WsHandleLoopPid, SocketMode, WsAutoExit) -> + handle_data(<<>>, T, Socket, WsHandleLoopPid, SocketMode, WsAutoExit); + +handle_data(none, <<>>, Socket, WsHandleLoopPid, SocketMode, WsAutoExit) -> + ws_loop(Socket, none, WsHandleLoopPid, SocketMode, WsAutoExit); + +handle_data(L, <<255,T/binary>>, Socket, WsHandleLoopPid, SocketMode, WsAutoExit) -> + WsHandleLoopPid ! {browser, iolist_to_binary(L)}, + handle_data(none, T, Socket, WsHandleLoopPid, SocketMode, WsAutoExit); + +handle_data(L, <>, Socket, WsHandleLoopPid, SocketMode, WsAutoExit) -> + handle_data(iolist_to_binary([L, H]), T, Socket, WsHandleLoopPid, SocketMode, WsAutoExit); + +handle_data(<<>>, L, Socket, WsHandleLoopPid, SocketMode, WsAutoExit) -> + ws_loop(Socket, L, 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 +%%% Purpose : Websocket support +%%% Created : 09-10-2010 by Eric Cestari +%%% Slightly adapted from : +% ========================================================================================================== +% MISULTIN - Websocket Request +% +% >-|-|-(°> +% +% Copyright (C) 2010, Roberto Ostinelli . +% 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/mod_http_bind.erl b/src/web/mod_http_bind.erl index 25e964f93..26c082645 100644 --- a/src/web/mod_http_bind.erl +++ b/src/web/mod_http_bind.erl @@ -117,7 +117,7 @@ start(_Host, _Opts) -> % mod_http_bind is already started so it will not be started again ok; {error, Error} -> - {'EXIT', {start_child_error, Error}} + exit({start_child_error, Error}) end. stop(_Host) -> @@ -125,14 +125,22 @@ stop(_Host) -> ok -> ok; {error, Error} -> - {'EXIT', {terminate_child_error, Error}} + exit({terminate_child_error, Error}) + end, + case supervisor:delete_child(ejabberd_sup, ejabberd_http_bind_sup) of + ok -> + ok; + {error, Error2} -> + exit({delete_child_error, Error2}) end. 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 @@ -142,4 +150,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/pshb_http.erl b/src/web/pshb_http.erl new file mode 100644 index 000000000..47efa0fc6 --- /dev/null +++ b/src/web/pshb_http.erl @@ -0,0 +1,414 @@ +%%%---------------------------------------------------------------------- +%%% File : pshb_http.erl +%%% Author : Eric Cestari +%%% 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'). + +-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}], + "\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: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: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: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 +% 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. \ 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..5928e7d83 --- /dev/null +++ b/src/web/websocket_test.erl @@ -0,0 +1,18 @@ +-module (websocket_test). +-export([start/1, loop/1]). + +% callback on received websockets data +start(Ws) -> + spawn(?MODULE, loop, [Ws]). + +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/xml_stream.erl b/src/xml_stream.erl index a918811cb..947992ad0 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). @@ -110,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/tools/joincluster b/tools/joincluster new file mode 100755 index 000000000..b6c610231 --- /dev/null +++ b/tools/joincluster @@ -0,0 +1,140 @@ +#!/bin/bash + +# Add the current ejabberd node in a cluster + +# copyright (c) 2010 ProcessOne +# +# This script is proprietary software and cannot be published or redistribute. + +# Return Code: +# 0 : groovy baby +# 11 : erl not found +# 12 : erlc not found +# 20 : database dir doesn't exist +# 21 : database dir not writable +# 21 : database dir variable not set +# 30 : network issue +# 31 : node names incompatibility + +function error +{ + echo "Error: $1" >&2 + exit $2 +} + +echo "--------------------------------------------------------------------" +echo "" +echo "ejabberd cluster configuration" +echo "" +echo "This ejabberd node will be configured for use in an ejabberd cluster." +echo "IMPORTANT: all local data from the database will be lost, and" +echo "cluster database will be initialized. All data from the master" +echo "node will be replicated to this one." +echo "" +echo "--------------------------------------------------------------------" +echo "Press any key to continue, or Ctrl+C to stop now" +read foo +echo "" + +echo "Make sure you have a running remote master ejabberd node" +echo "Before continuing, you must copy the ~/.erlang.cookie file from" +echo "remote master node and check ejabberd.cfg compatibility." +echo "e.g. hosts definition must match on all nodes" +echo "" +echo "The remote master node name is defined as ERLANG_NODE into" +echo "ejabberdctl.cfg on that remote node." +echo "" +echo -n "Remote master node name: " +read REMOTE +echo "" + +cont=Y +ping -q -c 1 ${REMOTE#*@} 2>/dev/null >/dev/null +[ $? -eq 0 ] || { + echo "Cannot ping ${REMOTE#*@}. Are you sure network setup is correct ?" + echo -n "Should we continue anyway ? (Y/n) " + read cont +} +cont=`echo $cont | tr a-z A-Z` +[ "$cont" == "Y" ] || error "Check your network configuration (dns, firewall, etc...)" 30 + +HERE=`which "$0"` +BASE=`dirname $HERE`/.. +ROOTDIR=`cd $BASE; pwd` +. $ROOTDIR/bin/ejabberdctl stop 2>/dev/null >/dev/null +NAME=-name +[ "$ERLANG_NODE" = "${ERLANG_NODE%.*}" ] && NAME=-sname +PA=/tmp/clustersetup_$$ +CLUSTERSETUP=clustersetup +CLUSTERSETUP_ERL=$PA/$CLUSTERSETUP.erl + +REMOTENAME=-name +[ "$REMOTE" = "${REMOTE%.*}" ] && REMOTENAME=-sname +[ "$REMOTENAME" = "$NAME" ] || { + echo "IMPORTANT!: node names are incompatible" + echo "Remote node name is $REMOTE" + echo "Local node name is $ERLANG_NODE" + echo "" + echo "Both node names must be short or fqdn names." + echo "Using short and fqdn names is impossible." + echo "" + error "incompatible node names" 31 +} + +set -o errexit +set -o nounset + +echo "Using commands:" +which erl || error "can't find erl" 11 +which erlc || error "can't find erlc" 12 +echo "" + +[ -d $EJABBERD_DB ] && rm -Rf $EJABBERD_DB +mkdir $EJABBERD_DB || error "$EJABBERD_DB cannot be created" 20 +[ -w $EJABBERD_DB ] || error "$EJABBERD_DB directory is not writable" 21 + +cd $ROOTDIR +mkdir -p $PA +cat < $CLUSTERSETUP_ERL +-module($CLUSTERSETUP). + +-export([start/0]). + +set_table_copy(Table, _Node, {badrpc, Reason}) -> + io:format("Error: cannot get storage type for table ~p on node $REMOTE:~n ~p~n",[Table, Reason]); +set_table_copy(Table, Node, Type) -> + io:format("setting table ~p to mode ~p~n",[Table, Type]), + case mnesia:add_table_copy(Table, Node, Type) of + {aborted, _} -> + mnesia:change_table_copy_type(Table, Node, Type); + _ -> + ok + end. + +set_tables({badrpc, Reason}) -> + io:format("ERROR: cannot get tables list on $REMOTE : ~p~n",[Reason]); +set_tables([]) -> + ok; +set_tables([schema | Tables]) -> + set_tables(Tables); +set_tables([s2s | Tables]) -> + set_tables(Tables); +set_tables([session | Tables]) -> + set_tables(Tables); +set_tables([Table | Tables]) -> + set_table_copy(Table, node(), + rpc:call('$REMOTE', mnesia, table_info, [Table, storage_type])), + set_tables(Tables). + +start() -> + io:format("~n",[]), + set_table_copy(schema, node(), disc_copies), + set_tables(rpc:call('$REMOTE', mnesia, system_info, [tables])), + halt(0). +EOF +erlc -o $PA $CLUSTERSETUP_ERL +erl $NAME $ERLANG_NODE -pa $PA $KERNEL_OPTS -mnesia extra_db_nodes "['$REMOTE']" dir "\"$EJABBERD_DB\"" -s mnesia -s $CLUSTERSETUP start +rm -Rf $PA + +echo "End." +echo "Check that there is no error in the above messages." diff --git a/tools/leavecluster b/tools/leavecluster new file mode 100755 index 000000000..f6b7b7ac8 --- /dev/null +++ b/tools/leavecluster @@ -0,0 +1,93 @@ +#!/bin/bash + +# Remove the current ejabberd node in a cluster + +# Return Code: +# 0 : groovy baby +# 11 : erl not found +# 12 : erlc not found +# 20 : database dir doesn't exist +# 21 : database dir not writable +# 21 : database dir variable not set + +function error +{ + echo "Error: $1" >&2 + exit $2 +} + +echo "--------------------------------------------------------------------" +echo "" +echo "ejabberd cluster configuration" +echo "" +echo "This ejabberd node will be removed from the cluster." +echo "IMPORTANT: this node will be stopped. At least one other clustered" +echo "node must be running." +echo "" +echo "--------------------------------------------------------------------" +echo "Press any key to continue, or Ctrl+C to stop now" +read foo +echo "" + +HERE=`which "$0"` +BASE=`dirname $HERE`/.. +ROOTDIR=`cd $BASE; pwd` +. $ROOTDIR/bin/ejabberdctl stop 2>/dev/null >/dev/null +$ROOTDIR/bin/ejabberdctl stopped +PA=/tmp/clustersetup_$$ +CLUSTERSETUP=clustersetup +CLUSTERSETUP_ERL=$PA/$CLUSTERSETUP.erl + +set -o errexit +set -o nounset + +echo "Using commands:" +which erl || error "can't find erl" 11 +which erlc || error "can't find erlc" 12 +echo "" + +cd $ROOTDIR +mkdir -p $PA +cat < $CLUSTERSETUP_ERL +-module($CLUSTERSETUP). + +-export([start/0]). + +del_table_copy(Table, Node) -> + case mnesia:del_table_copy(Table, Node) of + {aborted, Reason} -> io:format("Error: can not remove ~p table: ~p~n", [Table, Reason]); + _ -> io:format("table ~p removed from cluster~n", [Table]) + end. + +del_tables([],_) -> + ok; +del_tables([schema | Tables], Node) -> + del_tables(Tables, Node); +del_tables([Table | Tables], Node) -> + del_table_copy(Table, Node), + del_tables(Tables, Node). + +start() -> + io:format("~n",[]), + Removed = node(), + case mnesia:system_info(running_db_nodes)--[Removed] of + [] -> io:format("Error: no other node running in the cluster~n"); + Nodes -> + del_tables(mnesia:system_info(local_tables), Removed), + mnesia:stop(), + case rpc:call(hd(Nodes), mnesia, del_table_copy, [schema, Removed]) of + {badrpc,Reason} -> io:format("Error: can not unregister node ~p from cluster: ~p~n", [Removed, Reason]); + {aborted,Reason} -> io:format("Error: can not unregister node ~p from cluster: ~p~n", [Removed, Reason]); + {atomic, ok} -> + mnesia:delete_schema([Removed]), + io:format("node ~p removed from cluster~n", [Removed]) + end + end, + halt(0). +EOF +erlc -o $PA $CLUSTERSETUP_ERL +erl $NAME $ERLANG_NODE -pa $PA $KERNEL_OPTS -mnesia dir "\"$EJABBERD_DB\"" -s mnesia -s $CLUSTERSETUP start +rm -Rf $PA + +echo "End." +echo "Check that there is no error in the above messages." diff --git a/tools/tcp-tuning.sh b/tools/tcp-tuning.sh new file mode 100755 index 000000000..dcc2ef5ba --- /dev/null +++ b/tools/tcp-tuning.sh @@ -0,0 +1,38 @@ +#!/bin/sh +ulimit -n 562524 +# /etc/security/limits.conf +#@ejabberd soft nofile 562000 +#@ejabberd hard nofile 562000 +# 4KB send buffer, 20,480 connections max at worst case +#echo 83886080 > /proc/sys/net/core/wmem_max +#echo 83886080 > /proc/sys/net/core/wmem_default +echo 327680000 > /proc/sys/net/core/wmem_max +echo 327680000 > /proc/sys/net/core/wmem_default +# 16KB receive buffer, 20,480 connections max at worst case +#echo 335544320 > /proc/sys/net/core/rmem_max +#echo 335544320 > /proc/sys/net/core/rmem_default +echo 1310720000 > /proc/sys/net/core/rmem_max +echo 1310720000 > /proc/sys/net/core/rmem_default +# Max open files +echo 562524 > /proc/sys/fs/file-max +# Fast port recycling (TIME_WAIT) +echo 1 >/proc/sys/net/ipv4/tcp_tw_recycle +echo 1 >/proc/sys/net/ipv4/tcp_tw_reuse +# TIME_WAIT buckets increased +echo 360000 > /proc/sys/net/ipv4/tcp_max_tw_buckets +# FIN timeout decreased +echo 15 > /proc/sys/net/ipv4/tcp_fin_timeout +# SYN backlog increased +echo 65536 > /proc/sys/net/ipv4/tcp_max_syn_backlog +# SYN cookies enabled +echo 1 > /proc/sys/net/ipv4/tcp_syncookies +# Local port range maximized +echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range +# Netdev backlog increased +echo 100000 > /proc/sys/net/core/netdev_max_backlog +# Do no save route metrics +echo 1 > /proc/sys/net/ipv4/route/flush +echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout +# Interface transmit queuelen increased +ifconfig eth0 txqueuelen 10000 +