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 +%%% 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, 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 = <>, + 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 + + * 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 * doc/release_notes_2.0.4.txt: Added file for new release