diff --git a/ChangeLog b/ChangeLog index e902c2d05..bacf412c7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,13 @@ +2009-03-13 Evgeniy Khramtsov + + * 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. + * src/mod_muc/mod_muc_room.hrl: likewise. + 2009-03-10 Badlop * doc/release_notes_2.0.4.txt: Added file for new release diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index bfd505352..037306a62 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -375,6 +375,10 @@ process_term(Term, State) -> add_option(watchdog_large_heap, LH, 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 --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 84c83d3a1..62cd452a3 100644 --- a/src/jlib.hrl +++ b/src/jlib.hrl @@ -75,6 +75,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 --git a/src/mod_muc/mod_muc_room.erl b/src/mod_muc/mod_muc_room.erl index 34a602585..7a1618147 100644 --- a/src/mod_muc/mod_muc_room.erl +++ b/src/mod_muc/mod_muc_room.erl @@ -316,7 +316,8 @@ normal_state({route, From, "", (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); @@ -325,7 +326,9 @@ normal_state({route, From, "", ?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 @@ -685,6 +688,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 = 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}. @@ -1489,7 +1516,8 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> 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( @@ -1522,7 +1550,8 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> 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", @@ -1533,6 +1562,29 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> 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( @@ -1545,13 +1597,13 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = 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 @@ -1562,11 +1614,24 @@ check_password(_ServiceAffiliation, Els, StateData) -> Pass -> true; _ -> - false + false end 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]) -> @@ -2744,6 +2809,9 @@ get_config(Lang, StateData, From) -> ?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), @@ -2856,6 +2924,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) -> @@ -2947,6 +3017,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}}; @@ -2989,6 +3060,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), @@ -3112,6 +3184,17 @@ process_iq_disco_items(From, get, _Lang, StateData) -> {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 --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 815e918a7..74b930030 100644 --- a/src/web/ejabberd_http.erl +++ b/src/web/ejabberd_http.erl @@ -110,6 +110,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 -> []