Blob Blame History Raw
Binary files ejabberd-2.0.4/src/.DS_Store and ejabberd-2.0.4-new/src/.DS_Store differ
diff -urN ejabberd-2.0.4/src/ejabberd_captcha.erl ejabberd-2.0.4-new/src/ejabberd_captcha.erl
--- ejabberd-2.0.4/src/ejabberd_captcha.erl	1970-01-01 01:00:00.000000000 +0100
+++ ejabberd-2.0.4-new/src/ejabberd_captcha.erl	2009-03-14 07:27:05.000000000 +0100
@@ -0,0 +1,312 @@
+%%%-------------------------------------------------------------------
+%%% File    : ejabberd_captcha.erl
+%%% Author  : Evgeniy Khramtsov <xramtsov@gmail.com>
+%%% Description : CAPTCHA processing.
+%%%
+%%% Created : 26 Apr 2008 by Evgeniy Khramtsov <xramtsov@gmail.com>
+%%%-------------------------------------------------------------------
+-module(ejabberd_captcha).
+
+-behaviour(gen_server).
+
+%% API
+-export([start_link/0]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+	 terminate/2, code_change/3]).
+
+-export([create_captcha/6, process_reply/1, process/2]).
+
+-include("jlib.hrl").
+-include("ejabberd.hrl").
+-include("web/ejabberd_http.hrl").
+
+-define(VFIELD(Type, Var, Value),
+	{xmlelement, "field", [{"type", Type}, {"var", Var}],
+	 [{xmlelement, "value", [], [Value]}]}).
+
+-define(CAPTCHA_BODY(Lang, Room, URL),
+	translate:translate(Lang, "Your messages to ") ++ Room
+	++ translate:translate(Lang, " are being blocked. To unblock them, visit ")
+	++ URL).
+
+-define(CAPTCHA_TEXT(Lang), translate:translate(Lang, "Enter the text you see")).
+-define(CAPTCHA_LIFETIME, 120000). % two minutes
+
+-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
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
+%% Description: Starts the server
+%%--------------------------------------------------------------------
+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),
+       is_record(From, jid), is_record(To, jid) ->
+    case create_image() of
+	{ok, Type, Key, Image} ->
+	    B64Image = jlib:encode_base64(binary_to_list(Image)),
+	    JID = jlib:jid_to_string(From),
+	    CID = "sha1+" ++ sha:sha(Image) ++ "@bob.xmpp.org",
+	    Data = {xmlelement, "data",
+		    [{"xmlns", ?NS_BOB}, {"cid", CID},
+		     {"max-age", "0"}, {"type", Type}],
+		    [{xmlcdata, B64Image}]},
+	    Captcha =
+		{xmlelement, "captcha", [{"xmlns", ?NS_CAPTCHA}],
+		 [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}],
+		   [?VFIELD("hidden", "FORM_TYPE", {xmlcdata, ?NS_CAPTCHA}),
+		    ?VFIELD("hidden", "from", {xmlcdata, jlib:jid_to_string(To)}),
+		    ?VFIELD("hidden", "challenge", {xmlcdata, Id}),
+		    ?VFIELD("hidden", "sid", {xmlcdata, SID}),
+		    {xmlelement, "field", [{"var", "ocr"}],
+		     [{xmlelement, "media", [{"xmlns", ?NS_MEDIA}],
+		       [{xmlelement, "uri", [{"type", Type}],
+			 [{xmlcdata, "cid:" ++ CID}]}]}]}]}]},
+	    Body = {xmlelement, "body", [],
+		    [{xmlcdata, ?CAPTCHA_BODY(Lang, JID, get_url(Id))}]},
+	    OOB = {xmlelement, "x", [{"xmlns", ?NS_OOB}],
+		   [{xmlelement, "url", [], [{xmlcdata, get_url(Id)}]}]},
+	    Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}),
+	    ?T(mnesia:write(#captcha{id=Id, pid=self(), key=Key,
+				     tref=Tref, args=Args})),
+	    {ok, [Body, OOB, Captcha, Data]};
+	_Err ->
+	    error
+    end.
+
+process_reply({xmlelement, "captcha", _, _} = El) ->
+    case xml:get_subtag(El, "x") of
+	false ->
+	    {error, malformed};
+	Xdata ->
+	    Fields = jlib:parse_xdata_submit(Xdata),
+	    [Id | _] = proplists:get_value("challenge", Fields, [none]),
+	    [OCR | _] = proplists:get_value("ocr", Fields, [none]),
+	    ?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)
+    end;
+process_reply(_) ->
+    {error, malformed}.
+
+process(_Handlers, #request{method='GET', lang=Lang, path=[_, Id]}) ->
+    case mnesia:dirty_read(captcha, Id) of
+	[#captcha{}] ->
+	    Form =
+		{xmlelement, "div", [{"align", "center"}],
+		 [{xmlelement, "form", [{"action", get_url(Id)},
+					{"name", "captcha"},
+					{"method", "POST"}],
+		   [{xmlelement, "img", [{"src", get_url(Id ++ "/image")}], []},
+		    {xmlelement, "br", [], []},
+		    {xmlcdata, ?CAPTCHA_TEXT(Lang)},
+		    {xmlelement, "br", [], []},
+		    {xmlelement, "input", [{"type", "text"},
+					   {"name", "key"},
+					   {"size", "10"}], []},
+		    {xmlelement, "br", [], []},
+		    {xmlelement, "input", [{"type", "submit"},
+					   {"name", "enter"},
+					   {"value", "OK"}], []}]}]},
+	    ejabberd_web:make_xhtml([Form]);
+	_ ->
+	    ejabberd_web:error(not_found)
+    end;
+
+process(_Handlers, #request{method='GET', path=[_, Id, "image"]}) ->
+    case mnesia:dirty_read(captcha, Id) of
+	[#captcha{key=Key}] ->
+	    case create_image(Key) of
+		{ok, Type, _, Img} ->
+		    {200,
+		     [{"Content-Type", Type},
+		      {"Cache-Control", "no-cache"},
+		      {"Last-Modified", httpd_util:rfc1123_date()}],
+		     Img};
+		_ ->
+		    ejabberd_web:error(not_found)
+	    end;
+	_ ->
+	    ejabberd_web:error(not_found)
+    end;
+
+process(_Handlers, #request{method='POST', q=Q, path=[_, Id]}) ->
+    ?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),
+	       Input = proplists:get_value("key", Q, none),
+	       if Input == Key ->
+		       Pid ! {captcha_succeed, Args},
+		       ejabberd_web:make_xhtml([]);
+		  true ->
+		       Pid ! {captcha_failed, Args},
+		       ejabberd_web:error(not_allowed)
+	       end;
+	   _ ->
+	       ejabberd_web:error(not_found)
+       end).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+init([]) ->
+    mnesia:create_table(captcha,
+			[{ram_copies, [node()]},
+			 {attributes, record_info(fields, captcha)}]),
+    mnesia:add_table_copy(captcha, node(), ram_copies),
+    {ok, #state{}}.
+
+handle_call(_Request, _From, State) ->
+    {reply, bad_request, State}.
+
+handle_cast(_Msg, State) ->
+    {noreply, 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),
+    {noreply, State};
+
+handle_info(_Info, State) ->
+    {noreply, State}.
+
+terminate(_Reason, _State) ->
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+%%--------------------------------------------------------------------
+%% Function: create_image() -> {ok, Type, Key, Image} | {error, Reason}
+%% Type = "image/png" | "image/jpeg" | "image/gif"
+%% Key = string()
+%% Image = binary()
+%% Reason = atom()
+%%--------------------------------------------------------------------
+create_image() ->
+    %% Six numbers from 1 to 9.
+    Key = string:substr(randoms:get_string(), 1, 6),
+    create_image(Key).
+
+create_image(Key) ->
+    FileName = get_prog_name(),
+    Cmd = lists:flatten(io_lib:format("~s ~s", [FileName, Key])),
+    case cmd(Cmd) of
+	{ok, <<16#89, $P, $N, $G, $\r, $\n, 16#1a, $\n, _/binary>> = Img} ->
+	    {ok, "image/png", Key, Img};
+	{ok, <<16#ff, 16#d8, _/binary>> = Img} ->
+	    {ok, "image/jpeg", Key, Img};
+	{ok, <<$G, $I, $F, $8, X, $a, _/binary>> = Img} when X==$7; X==$9 ->
+	    {ok, "image/gif", Key, Img};
+	{error, Reason} ->
+	    ?ERROR_MSG("Failed to process an output from \"~s\": ~p",
+		       [Cmd, Reason]),
+	    {error, Reason};
+	_ ->
+	    Reason = malformed_image,
+	    ?ERROR_MSG("Failed to process an output from \"~s\": ~p",
+		       [Cmd, Reason]),
+	    {error, Reason}
+    end.
+
+get_prog_name() ->
+    case ejabberd_config:get_local_option(captcha_cmd) of
+	FileName when is_list(FileName) ->
+	    FileName;
+	_ ->
+	    ""
+    end.
+
+get_url(Str) ->
+    case ejabberd_config:get_local_option(captcha_host) of
+	Host when is_list(Host) ->
+	    "http://" ++ Host ++ "/captcha/" ++ Str;
+	_ ->
+	    "http://" ++ ?MYNAME ++ "/captcha/" ++ Str
+    end.
+
+%%--------------------------------------------------------------------
+%% Function: cmd(Cmd) -> Data | {error, Reason}
+%% Cmd = string()
+%% Data = binary()
+%% Description: os:cmd/1 replacement
+%%--------------------------------------------------------------------
+-define(CMD_TIMEOUT, 5000).
+-define(MAX_FILE_SIZE, 64*1024).
+
+cmd(Cmd) ->
+    Port = open_port({spawn, Cmd}, [stream, eof, binary]),
+    TRef = erlang:start_timer(?CMD_TIMEOUT, self(), timeout),
+    recv_data(Port, TRef, <<>>).
+
+recv_data(Port, TRef, Buf) ->
+    receive
+	{Port, {data, Bytes}} ->
+	    NewBuf = <<Buf/binary, Bytes/binary>>,
+	    if size(NewBuf) > ?MAX_FILE_SIZE ->
+		    return(Port, TRef, {error, efbig});
+	       true ->
+		    recv_data(Port, TRef, NewBuf)
+	    end;
+	{Port, {data, _}} ->
+	    return(Port, TRef, {error, efbig});
+	{Port, eof} when Buf /= <<>> ->
+	    return(Port, TRef, {ok, Buf});
+	{Port, eof} ->
+	    return(Port, TRef, {error, enodata});
+	{timeout, TRef, _} ->
+	    return(Port, TRef, {error, timeout})
+    end.
+
+return(Port, TRef, Result) ->
+    case erlang:cancel_timer(TRef) of
+	false ->
+	    receive
+		{timeout, TRef, _} ->
+		    ok
+	    after 0 ->
+		    ok
+	    end;
+	_ ->
+	    ok
+    end,
+    catch port_close(Port),
+    Result.
diff -urN ejabberd-2.0.4/src/ejabberd_config.erl ejabberd-2.0.4-new/src/ejabberd_config.erl
--- ejabberd-2.0.4/src/ejabberd_config.erl	2009-03-12 09:41:02.000000000 +0100
+++ ejabberd-2.0.4-new/src/ejabberd_config.erl	2009-03-14 11:43:35.000000000 +0100
@@ -164,6 +164,10 @@
 	    add_option(watchdog_admins, Admins, State);
 	{registration_timeout, Timeout} ->
 	    add_option(registration_timeout, Timeout, State);
+	{captcha_cmd, Cmd} ->
+	    add_option(captcha_cmd, Cmd, State);
+	{captcha_host, Host} ->
+            add_option(captcha_host, Host, State);
 	{loglevel, Loglevel} ->
 	    ejabberd_loglevel:set(Loglevel),
 	    State;
diff -urN ejabberd-2.0.4/src/ejabberd_sup.erl ejabberd-2.0.4-new/src/ejabberd_sup.erl
--- ejabberd-2.0.4/src/ejabberd_sup.erl	2009-03-12 09:41:02.000000000 +0100
+++ ejabberd-2.0.4-new/src/ejabberd_sup.erl	2009-03-14 12:36:43.000000000 +0100
@@ -84,6 +84,13 @@
 	 brutal_kill,
 	 worker,
 	 [ejabberd_local]},
+    Captcha =
+	{ejabberd_captcha,
+	 {ejabberd_captcha, start_link, []},
+	 permanent,
+	 brutal_kill,
+	 worker,
+	 [ejabberd_captcha]},
     Listener =
 	{ejabberd_listener,
 	 {ejabberd_listener, start_link, []},
@@ -170,6 +177,7 @@
 	   SM,
 	   S2S,
 	   Local,
+           Captcha,
 	   ReceiverSupervisor,
 	   C2SSupervisor,
 	   S2SInSupervisor,
diff -urN ejabberd-2.0.4/src/jlib.hrl ejabberd-2.0.4-new/src/jlib.hrl
--- ejabberd-2.0.4/src/jlib.hrl	2009-03-12 09:41:02.000000000 +0100
+++ ejabberd-2.0.4-new/src/jlib.hrl	2009-03-14 11:41:46.000000000 +0100
@@ -74,6 +74,12 @@
 
 -define(NS_CAPS,          "http://jabber.org/protocol/caps").
 
+%% CAPTCHA related NSes.
+-define(NS_OOB, "jabber:x:oob").
+-define(NS_CAPTCHA, "urn:xmpp:captcha").
+-define(NS_MEDIA, "urn:xmpp:media-element").
+-define(NS_BOB, "urn:xmpp:bob").
+
 % TODO: remove "code" attribute (currently it used for backward-compatibility)
 -define(STANZA_ERROR(Code, Type, Condition),
 	{xmlelement, "error",
diff -urN ejabberd-2.0.4/src/mod_muc/mod_muc_room.erl ejabberd-2.0.4-new/src/mod_muc/mod_muc_room.erl
--- ejabberd-2.0.4/src/mod_muc/mod_muc_room.erl	2009-03-12 09:41:02.000000000 +0100
+++ ejabberd-2.0.4-new/src/mod_muc/mod_muc_room.erl	2009-03-14 11:40:40.000000000 +0100
@@ -69,6 +69,7 @@
 		 public_list = true,
 		 persistent = false,
 		 moderated = true,
+		 captcha_protected = false,
 		 members_by_default = true,
 		 members_only = false,
 		 allow_user_invites = false,
@@ -98,6 +99,7 @@
 		jid,
 		config = #config{},
 		users = ?DICT:new(),
+		robots = ?DICT:new(),
 		affiliations = ?DICT:new(),
 		history = lqueue_new(20),
 		subject = "",
@@ -382,7 +384,8 @@
 	      (XMLNS == ?NS_MUC_ADMIN) or
 	      (XMLNS == ?NS_MUC_OWNER) or
 	      (XMLNS == ?NS_DISCO_INFO) or
-	      (XMLNS == ?NS_DISCO_ITEMS) ->
+	      (XMLNS == ?NS_DISCO_ITEMS) or 
+	      (XMLNS == ?NS_CAPTCHA) ->
 	    Res1 = case XMLNS of
 		       ?NS_MUC_ADMIN ->
 			   process_iq_admin(From, Type, Lang, SubEl, StateData);
@@ -391,7 +394,9 @@
 		       ?NS_DISCO_INFO ->
 			   process_iq_disco_info(From, Type, Lang, StateData);
 		       ?NS_DISCO_ITEMS ->
-			   process_iq_disco_items(From, Type, Lang, StateData)
+			   process_iq_disco_items(From, Type, Lang, StateData);
+		       ?NS_CAPTCHA ->
+			   process_iq_captcha(From, Type, Lang, SubEl, StateData)
 		   end,
 	    {IQRes, NewStateData} =
 		case Res1 of
@@ -761,6 +766,30 @@
 	{empty, _} ->
 	    {next_state, StateName, StateData}
     end;
+handle_info({captcha_succeed, From}, normal_state, StateData) ->
+    NewState = case ?DICT:find(From, StateData#state.robots) of
+		   {ok, {Nick, Packet}} ->
+		       Robots = ?DICT:store(From, passed, StateData#state.robots),
+		       add_new_user(From, Nick, Packet, StateData#state{robots=Robots});
+		   _ ->
+		       StateData
+	       end,
+    {next_state, normal_state, NewState};
+handle_info({captcha_failed, From}, normal_state, StateData) ->
+    NewState = case ?DICT:find(From, StateData#state.robots) of
+		   {ok, {Nick, Packet}} ->
+		       Robots = ?DICT:erase(From, StateData#state.robots),
+		       Err = jlib:make_error_reply(
+			       Packet, ?ERR_NOT_AUTHORIZED),
+		       ejabberd_router:route( % TODO: s/Nick/""/
+			 jlib:jid_replace_resource(
+			   StateData#state.jid, Nick),
+			 From, Err),
+		       StateData#state{robots=Robots};
+		   _ ->
+		       StateData
+	       end,
+    {next_state, normal_state, NewState};
 handle_info(_Info, StateName, StateData) ->
     {next_state, StateName, StateData}.
 
@@ -1461,7 +1490,8 @@
 	      From, Err),
 	    StateData;
 	{_, _, _, Role} ->
-	    case check_password(ServiceAffiliation, Els, StateData) of
+	    case check_password(ServiceAffiliation, Affiliation,
+				Els, From, StateData) of
 		true ->
 		    NewState =
 			add_user_presence(
@@ -1494,7 +1524,8 @@
 			true ->
 			    NewState#state{just_created = false};
 			false ->
-			    NewState
+			    Robots = ?DICT:erase(From, StateData#state.robots),
+			    NewState#state{robots = Robots}
 		    end;
 		nopass ->
 		    ErrText = "Password required to enter this room",
@@ -1505,6 +1536,29 @@
 			StateData#state.jid, Nick),
 		      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} ->
+			    MsgPkt = {xmlelement, "message", [{"id", ID}], CaptchaEls},
+			    Robots = ?DICT:store(From,
+						 {Nick, Packet}, StateData#state.robots),
+			    ejabberd_router:route(RoomJID, From, MsgPkt),
+			    StateData#state{robots = Robots};
+			error ->
+			    ErrText = "Unable to generate a captcha",
+			    Err = jlib:make_error_reply(
+				    Packet, ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText)),
+			    ejabberd_router:route( % TODO: s/Nick/""/
+			      jlib:jid_replace_resource(
+				StateData#state.jid, Nick),
+			      From, Err),
+			    StateData
+		    end;
 		_ ->
 		    ErrText = "Incorrect password",
 		    Err = jlib:make_error_reply(
@@ -1517,13 +1571,13 @@
 	   end
     end.
 
-check_password(owner, _Els, _StateData) ->
+check_password(owner, _Affiliation, _Els, _From, _StateData) ->
     %% Don't check pass if user is owner in MUC service (access_admin option)
     true;
-check_password(_ServiceAffiliation, Els, StateData) ->
+check_password(_ServiceAffiliation, Affiliation, Els, From, StateData) ->
     case (StateData#state.config)#config.password_protected of
 	false ->
-	    true;
+	    check_captcha(Affiliation, From, StateData);
 	true ->
 	    Pass = extract_password(Els),
 	    case Pass of
@@ -1539,6 +1593,19 @@
 	    end
     end.
 
+check_captcha(Affiliation, From, StateData) ->
+    case (StateData#state.config)#config.captcha_protected of
+	true when Affiliation == none ->
+	    case ?DICT:find(From, StateData#state.robots) of
+		{ok, passed} ->
+		    true;
+		_ ->
+		    captcha_required
+	    end;
+	_ ->
+	    true
+    end.
+
 extract_password([]) ->
     false;
 extract_password([{xmlelement, _Name, Attrs, _SubEls} = El | Els]) ->
@@ -2713,6 +2780,9 @@
 	 ?BOOLXFIELD("Make room members-only",
 		     "muc#roomconfig_membersonly",
 		     Config#config.members_only),
+	 ?BOOLXFIELD("Make room captcha protected",
+		     "captcha_protected",
+		     Config#config.captcha_protected),
 	 ?BOOLXFIELD("Make room moderated",
 		     "muc#roomconfig_moderatedroom",
 		     Config#config.moderated),
@@ -2823,6 +2893,8 @@
     ?SET_BOOL_XOPT(members_by_default, Val);
 set_xoption([{"muc#roomconfig_membersonly", [Val]} | Opts], Config) ->
     ?SET_BOOL_XOPT(members_only, Val);
+set_xoption([{"captcha_protected", [Val]} | Opts], Config) ->
+    ?SET_BOOL_XOPT(captcha_protected, Val);
 set_xoption([{"muc#roomconfig_allowinvites", [Val]} | Opts], Config) ->
     ?SET_BOOL_XOPT(allow_user_invites, Val);
 set_xoption([{"muc#roomconfig_passwordprotectedroom", [Val]} | Opts], Config) ->
@@ -2913,6 +2985,7 @@
 	      ?CASE_CONFIG_OPT(members_only);
 	      ?CASE_CONFIG_OPT(allow_user_invites);
 	      ?CASE_CONFIG_OPT(password_protected);
+	      ?CASE_CONFIG_OPT(captcha_protected);
 	      ?CASE_CONFIG_OPT(password);
 	      ?CASE_CONFIG_OPT(anonymous);
 	      ?CASE_CONFIG_OPT(logging);
@@ -2954,6 +3027,7 @@
      ?MAKE_CONFIG_OPT(members_only),
      ?MAKE_CONFIG_OPT(allow_user_invites),
      ?MAKE_CONFIG_OPT(password_protected),
+     ?MAKE_CONFIG_OPT(captcha_protected),
      ?MAKE_CONFIG_OPT(password),
      ?MAKE_CONFIG_OPT(anonymous),
      ?MAKE_CONFIG_OPT(logging),
@@ -3074,6 +3148,17 @@
 	    {error, ?ERR_FORBIDDEN}
     end.
 
+process_iq_captcha(_From, get, _Lang, _SubEl, _StateData) ->
+    {error, ?ERR_NOT_ALLOWED};
+
+process_iq_captcha(_From, set, _Lang, SubEl, StateData) ->
+    case ejabberd_captcha:process_reply(SubEl) of
+	ok ->
+	    {result, [], StateData};
+	_ ->
+	    {error, ?ERR_NOT_ACCEPTABLE}
+    end.
+
 get_title(StateData) ->
     case (StateData#state.config)#config.title of
 	"" ->
diff -urN ejabberd-2.0.4/src/web/ejabberd_http.erl ejabberd-2.0.4-new/src/web/ejabberd_http.erl
--- ejabberd-2.0.4/src/web/ejabberd_http.erl	2009-03-12 09:41:02.000000000 +0100
+++ ejabberd-2.0.4-new/src/web/ejabberd_http.erl	2009-03-14 10:57:34.000000000 +0100
@@ -106,6 +106,10 @@
 	    {value, {request_handlers, H}} -> H;
 	    false -> []
         end ++
+	case lists:member(captcha, Opts) of
+	    true -> [{["captcha"], ejabberd_captcha}];
+	    false -> []
+	end ++
         case lists:member(web_admin, Opts) of
             true -> [{["admin"], ejabberd_web_admin}];
             false -> []
Binary files ejabberd-2.0.4/tools/.DS_Store and ejabberd-2.0.4-new/tools/.DS_Store differ
diff -urN ejabberd-2.0.4/tools/captcha.sh ejabberd-2.0.4-new/tools/captcha.sh
--- ejabberd-2.0.4/tools/captcha.sh	1970-01-01 01:00:00.000000000 +0100
+++ ejabberd-2.0.4-new/tools/captcha.sh	2009-03-14 11:46:42.000000000 +0100
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+SIGN=$(($RANDOM % 2))
+
+R1=$(($RANDOM % 20))
+R2=$(($RANDOM % 10 + 40))
+
+if [ $SIGN -eq "0" ]; then
+    S1=$(( -1*($RANDOM % 20 + 50) ))
+    S2=$(( $RANDOM % 20 + 50 ))
+else
+    S2=$(( -1*($RANDOM % 20 + 50) ))
+    S1=$(( $RANDOM % 20 + 50 ))
+fi
+
+convert -size 140x60 xc:white \
+    -pointsize 30 -draw "text 20,30 '$1'" \
+    -roll -$R2+$R1 -swirl $S1 \
+    -roll +$R2-$R1 -swirl $S2 \
+    +repage -resize 120x60 \
+    -quality 90 -depth 8 png:-
--- ejabberd-2.0.5/ChangeLog~	2009-04-01 19:23:52.000000000 +0400
+++ ejabberd-2.0.5/ChangeLog	2009-04-03 23:45:03.174979944 +0400
@@ -15,6 +15,15 @@
 	stanza (EJAB-300).
 	* src/ejabberd_c2s.erl: Likewise
 
+2009-03-13  Evgeniy Khramtsov <ekhramtsov@process-one.net>
+
+	* src/ejabberd_captcha.erl: XEP-158 (CAPTCHA Forms).
+	* src/ejabberd_config.erl: likewise.
+	* src/ejabberd_sup.erl: likewise.
+	* src/jlib.hrl: likewise.
+	* src/web/ejabberd_http.erl: likewise.
+	* src/mod_muc/mod_muc_room.erl: CAPTCHA support.
+
 2009-03-10  Badlop  <badlop@process-one.net>
 
 	* doc/release_notes_2.0.4.txt: Added file for new release