diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl new file mode 100644 index 000000000..8813d82d2 --- /dev/null +++ b/src/ejabberd_captcha.erl @@ -0,0 +1,389 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_captcha.erl +%%% Author : Evgeniy Khramtsov +%%% Description : CAPTCHA processing. +%%% +%%% Created : 26 Apr 2008 by Evgeniy Khramtsov +%%%------------------------------------------------------------------- +-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, build_captcha_html/2, check_captcha/2, + process_reply/1, process/2, is_feature_available/0]). + +-include_lib("exmpp/include/exmpp.hrl"). + +-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_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(SID) -> + case create_image() of + {ok, Type, Key, Image} -> + B64Image = jlib:encode_base64(binary_to_list(Image)), + JID = exmpp_jid:jid_to_list(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}], + %% ?NS_DATA_FORMS is 'jabber:x:data' + [{xmlelement, "x", [{"xmlns", "jabber:x:data"}, {"type", "form"}], + [?VFIELD("hidden", "FORM_TYPE", {xmlcdata, ?NS_CAPTCHA}), + ?VFIELD("hidden", "from", {xmlcdata, exmpp_jid:jid_to_list(To)}), + ?VFIELD("hidden", "challenge", {xmlcdata, Id}), + ?VFIELD("hidden", "sid", {xmlcdata, SID}), + {xmlelement, "field", [{"var", "ocr"}, {"label", ?CAPTCHA_TEXT(Lang)}], + [{xmlelement, "media", [{"xmlns", ?NS_MEDIA}], + [{xmlelement, "uri", [{"type", Type}], + [{xmlcdata, "cid:" ++ CID}]}]}]}]}]}, + BodyString1 = translate:translate(Lang, "Your messages to ~s are being blocked. To unblock them, visit ~s"), + BodyString = io_lib:format(BodyString1, [JID, get_url(Id)]), + Body = {xmlelement, "body", [], + [{xmlcdata, BodyString}]}, + 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; + _Err -> + error + end. + +%% @spec (Id::string(), Lang::string()) -> {FormEl, {ImgEl, TextEl, IdEl, KeyEl}} | captcha_not_found +%% where FormEl = xmlelement() +%% ImgEl = xmlelement() +%% TextEl = xmlelement() +%% IdEl = xmlelement() +%% KeyEl = xmlelement() +build_captcha_html(Id, Lang) -> + case mnesia:dirty_read(captcha, Id) of + [#captcha{}] -> + ImgEl = {xmlelement, "img", [{"src", get_url(Id ++ "/image")}], []}, + TextEl = {xmlcdata, ?CAPTCHA_TEXT(Lang)}, + IdEl = {xmlelement, "input", [{"type", "hidden"}, + {"name", "id"}, + {"value", Id}], []}, + KeyEl = {xmlelement, "input", [{"type", "text"}, + {"name", "key"}, + {"size", "10"}], []}, + FormEl = {xmlelement, "form", [{"action", get_url(Id)}, + {"name", "captcha"}, + {"method", "POST"}], + [ImgEl, + {xmlelement, "br", [], []}, + TextEl, + {xmlelement, "br", [], []}, + IdEl, + KeyEl, + {xmlelement, "br", [], []}, + {xmlelement, "input", [{"type", "submit"}, + {"name", "enter"}, + {"value", "OK"}], []} + ]}, + {FormEl, {ImgEl, TextEl, IdEl, KeyEl}}; + _ -> + captcha_not_found + end. + +%% @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). + +process_reply(El) -> + case {exmpp_xml:element_matches(El, captcha), + exmpp_xml:get_element(El, x)} of + {false, _} -> + {error, malformed}; + {_, undefined} -> + {error, malformed}; + {true, 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(_Handlers, #request{method='GET', lang=Lang, path=[_, Id]}) -> + case build_captcha_html(Id, Lang) of + {FormEl, _} when is_tuple(FormEl) -> + Form = + {xmlelement, "div", [{"align", "center"}], + [FormEl]}, + ejabberd_web:make_xhtml([Form]); + captcha_not_found -> + 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, lang=Lang, path=[_, Id]}) -> + ProvidedKey = proplists:get_value("key", Q, none), + case check_captcha(Id, ProvidedKey) of + captcha_valid -> + Form = + {xmlelement, "p", [], + [{xmlcdata, + translate:translate(Lang, "The captcha is valid.") + }]}, + ejabberd_web:make_xhtml([Form]); + captcha_non_valid -> + ejabberd_web:error(not_allowed); + captcha_not_found -> + 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), + check_captcha_setup(), + {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, enodata = Reason} -> + ?ERROR_MSG("Failed to process output from \"~s\". " + "Maybe ImageMagick's Convert program is not installed.", + [Cmd]), + {error, Reason}; + {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 = <>, + 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. + +is_feature_enabled() -> + case get_prog_name() of + "" -> false; + Prog when is_list(Prog) -> true + end. + +is_feature_available() -> + case is_feature_enabled() of + false -> false; + true -> + case create_image() of + {ok, _, _, _} -> true; + _Error -> false + end + end. + +check_captcha_setup() -> + case is_feature_enabled() andalso not is_feature_available() of + true -> + ?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, " + "but it can't generate images.", []); + false -> + ok + end. diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index a5b47ed94..883e484b5 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -378,6 +378,10 @@ process_term(Term, State) -> add_option(registration_timeout, Timeout, State); {ejabberdctl_access_commands, ACs} -> add_option(ejabberdctl_access_commands, ACs, 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 --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl index 7a650cc35..6163dfee0 100644 --- a/src/ejabberd_sup.erl +++ b/src/ejabberd_sup.erl @@ -84,6 +84,13 @@ init([]) -> 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 @@ init([]) -> SM, S2S, Local, + Captcha, ReceiverSupervisor, C2SSupervisor, S2SInSupervisor, diff --git a/src/jlib.hrl b/src/jlib.hrl index 479781431..02e419b28 100644 --- a/src/jlib.hrl +++ b/src/jlib.hrl @@ -19,5 +19,11 @@ %%% %%%---------------------------------------------------------------------- +%% 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"). + -record(rsm_in, {max, direction, id, index}). -record(rsm_out, {count, index, first, last}). diff --git a/src/mod_muc/mod_muc_room.erl b/src/mod_muc/mod_muc_room.erl index 033c537a9..6c76008e5 100644 --- a/src/mod_muc/mod_muc_room.erl +++ b/src/mod_muc/mod_muc_room.erl @@ -50,6 +50,7 @@ -include("ejabberd.hrl"). -include("mod_muc_room.hrl"). +-include("jlib.hrl"). %% Used for captcha -define(MAX_USERS_DEFAULT_LIST, [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). @@ -329,7 +330,8 @@ normal_state({route, From, undefined, (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); @@ -338,7 +340,9 @@ normal_state({route, From, undefined, ?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 @@ -707,6 +711,30 @@ handle_info(process_room_queue, normal_state = StateName, StateData) -> {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 = exmpp_stanza:reply_with_error( + Packet, ?ERR(Packet, 'not-authorized', undefined, "")), + ejabberd_router:route( % TODO: s/Nick/""/ + 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}. @@ -1512,8 +1540,8 @@ add_new_user(From, Nick, Packet, StateData) -> From, Err), StateData; {_, _, _, Role} -> - case check_password(ServiceAffiliation, - exmpp_xml:get_child_elements(Packet), + case check_password(ServiceAffiliation, Affiliation, + exmpp_xml:get_child_elements(Packet), From, StateData) of true -> NewState = @@ -1550,7 +1578,8 @@ add_new_user(From, Nick, Packet, StateData) -> true -> NewState#state{just_created = false}; false -> - NewState + Robots = ?DICT:erase(From, StateData#state.robots), + NewState#state{robots = Robots} end; nopass -> ErrText = "The password is required to enter this room", @@ -1561,6 +1590,29 @@ add_new_user(From, Nick, Packet, StateData) -> StateData#state.jid, Nick), From, Err), StateData; + captcha_required -> + ID = randoms:get_string(), + SID = case exmpp_stanza:get_id(Packet) of undefined -> ""; SID1 -> SID1 end, + RoomJID = StateData#state.jid, + To = 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 = exmpp_stanza:reply_with_error( + Packet, ?ERR(Packet, 'internal-server-error', Lang, ErrText)), + ejabberd_router:route( % TODO: s/Nick/""/ + jid_replace_resource( + StateData#state.jid, Nick), + From, Err), + StateData + end; _ -> ErrText = "Incorrect password", Err = exmpp_stanza:reply_with_error( @@ -1573,13 +1625,13 @@ add_new_user(From, Nick, Packet, StateData) -> 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 @@ -1590,11 +1642,25 @@ check_password(_ServiceAffiliation, Els, StateData) -> Pass -> true; _ -> - false + false end end end. +check_captcha(Affiliation, From, StateData) -> + case (StateData#state.config)#config.captcha_protected + andalso ejabberd_captcha:is_feature_available() 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([#xmlel{ns = XMLNS} = El | Els]) -> @@ -2758,8 +2824,9 @@ get_config(Lang, StateData, From) -> end, Res = [#xmlel{name = 'title', children = [ #xmlcdata{cdata = - translate:translate(Lang, "Configuration for ") ++ - exmpp_jid:jid_to_list(StateData#state.jid)}]}, + io_lib:format(translate:translate(Lang, "Configuration of room ~s"), + [exmpp_jid:jid_to_list(StateData#state.jid)]) + }]}, #xmlel{name = 'field', attrs = [?XMLATTR('type', <<"hidden">>), ?XMLATTR('var', <<"FORM_TYPE">>)], children = [#xmlel{name = 'value', children = [#xmlcdata{cdata = @@ -2865,9 +2932,14 @@ get_config(Lang, StateData, From) -> ?BOOLXFIELD("Allow visitors to change nickname", "muc#roomconfig_allowvisitornickchange", Config#config.allow_visitor_nickchange) - ] ++ - - + ] ++ + case ejabberd_captcha:is_feature_available() of + true -> + [?BOOLXFIELD("Make room captcha protected", + "captcha_protected", + Config#config.captcha_protected)]; + false -> [] + end ++ case mod_muc_log:check_access_log( StateData#state.server_host, From) of allow -> @@ -2954,6 +3026,8 @@ set_xoption([{"members_by_default", [Val]} | Opts], Config) -> ?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) -> @@ -3045,6 +3119,7 @@ set_opts([{Opt, Val} | Opts], StateData) -> members_only -> StateData#state{config = (StateData#state.config)#config{members_only = Val}}; allow_user_invites -> StateData#state{config = (StateData#state.config)#config{allow_user_invites = Val}}; password_protected -> StateData#state{config = (StateData#state.config)#config{password_protected = Val}}; + captcha_protected -> StateData#state{config = (StateData#state.config)#config{captcha_protected = Val}}; password -> StateData#state{config = (StateData#state.config)#config{password = Val}}; anonymous -> StateData#state{config = (StateData#state.config)#config{anonymous = Val}}; logging -> StateData#state{config = (StateData#state.config)#config{logging = Val}}; @@ -3087,6 +3162,7 @@ make_opts(StateData) -> ?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), @@ -3226,6 +3302,17 @@ process_iq_disco_items(From, get, _Lang, StateData) -> {error, 'forbidden'} end. +process_iq_captcha(_From, get, _Lang, _SubEl, _StateData) -> + {error, 'not-allowed'}; + +process_iq_captcha(_From, set, _Lang, SubEl, StateData) -> + case ejabberd_captcha:process_reply(SubEl) of + ok -> + {result, [], StateData}; + _ -> + {error, 'not-acceptable'} + end. + get_title(StateData) -> case (StateData#state.config)#config.title of "" -> diff --git a/src/mod_muc/mod_muc_room.hrl b/src/mod_muc/mod_muc_room.hrl index 3acf1013a..2ff1c1814 100644 --- a/src/mod_muc/mod_muc_room.hrl +++ b/src/mod_muc/mod_muc_room.hrl @@ -37,6 +37,7 @@ public_list = true, persistent = false, moderated = true, + captcha_protected = false, members_by_default = true, members_only = false, allow_user_invites = false, @@ -66,6 +67,7 @@ jid, config = #config{}, users = ?DICT:new(), + robots = ?DICT:new(), affiliations = ?DICT:new(), history, subject = "", diff --git a/src/web/ejabberd_http.erl b/src/web/ejabberd_http.erl index 71895bf3f..0310737b5 100644 --- a/src/web/ejabberd_http.erl +++ b/src/web/ejabberd_http.erl @@ -114,6 +114,10 @@ start_link({SockMod, Socket}, Opts) -> {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 -> [] diff --git a/tools/captcha.sh b/tools/captcha.sh new file mode 100644 index 000000000..2b18d93ad --- /dev/null +++ b/tools/captcha.sh @@ -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:-