mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-30 16:36:29 +01:00
Implement XEP-158 CAPTCHA Forms, support in mod_muc, sample script (thanks to Evgeniy Khramtsov)(EJAB-895)
SVN Revision: 2102
This commit is contained in:
parent
18ae44f930
commit
55bebb0f62
389
src/ejabberd_captcha.erl
Normal file
389
src/ejabberd_captcha.erl
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% 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, 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 = <<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.
|
||||||
|
|
||||||
|
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.
|
@ -378,6 +378,10 @@ process_term(Term, State) ->
|
|||||||
add_option(registration_timeout, Timeout, State);
|
add_option(registration_timeout, Timeout, State);
|
||||||
{ejabberdctl_access_commands, ACs} ->
|
{ejabberdctl_access_commands, ACs} ->
|
||||||
add_option(ejabberdctl_access_commands, ACs, State);
|
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} ->
|
{loglevel, Loglevel} ->
|
||||||
ejabberd_loglevel:set(Loglevel),
|
ejabberd_loglevel:set(Loglevel),
|
||||||
State;
|
State;
|
||||||
|
@ -84,6 +84,13 @@ init([]) ->
|
|||||||
brutal_kill,
|
brutal_kill,
|
||||||
worker,
|
worker,
|
||||||
[ejabberd_local]},
|
[ejabberd_local]},
|
||||||
|
Captcha =
|
||||||
|
{ejabberd_captcha,
|
||||||
|
{ejabberd_captcha, start_link, []},
|
||||||
|
permanent,
|
||||||
|
brutal_kill,
|
||||||
|
worker,
|
||||||
|
[ejabberd_captcha]},
|
||||||
Listener =
|
Listener =
|
||||||
{ejabberd_listener,
|
{ejabberd_listener,
|
||||||
{ejabberd_listener, start_link, []},
|
{ejabberd_listener, start_link, []},
|
||||||
@ -170,6 +177,7 @@ init([]) ->
|
|||||||
SM,
|
SM,
|
||||||
S2S,
|
S2S,
|
||||||
Local,
|
Local,
|
||||||
|
Captcha,
|
||||||
ReceiverSupervisor,
|
ReceiverSupervisor,
|
||||||
C2SSupervisor,
|
C2SSupervisor,
|
||||||
S2SInSupervisor,
|
S2SInSupervisor,
|
||||||
|
@ -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_in, {max, direction, id, index}).
|
||||||
-record(rsm_out, {count, index, first, last}).
|
-record(rsm_out, {count, index, first, last}).
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
|
|
||||||
-include("ejabberd.hrl").
|
-include("ejabberd.hrl").
|
||||||
-include("mod_muc_room.hrl").
|
-include("mod_muc_room.hrl").
|
||||||
|
-include("jlib.hrl"). %% Used for captcha
|
||||||
|
|
||||||
-define(MAX_USERS_DEFAULT_LIST,
|
-define(MAX_USERS_DEFAULT_LIST,
|
||||||
[5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]).
|
[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_ADMIN) or
|
||||||
(XMLNS == ?NS_MUC_OWNER) or
|
(XMLNS == ?NS_MUC_OWNER) or
|
||||||
(XMLNS == ?NS_DISCO_INFO) or
|
(XMLNS == ?NS_DISCO_INFO) or
|
||||||
(XMLNS == ?NS_DISCO_ITEMS) ->
|
(XMLNS == ?NS_DISCO_ITEMS) or
|
||||||
|
(XMLNS == ?NS_CAPTCHA) ->
|
||||||
Res1 = case XMLNS of
|
Res1 = case XMLNS of
|
||||||
?NS_MUC_ADMIN ->
|
?NS_MUC_ADMIN ->
|
||||||
process_iq_admin(From, Type, Lang, SubEl, StateData);
|
process_iq_admin(From, Type, Lang, SubEl, StateData);
|
||||||
@ -338,7 +340,9 @@ normal_state({route, From, undefined,
|
|||||||
?NS_DISCO_INFO ->
|
?NS_DISCO_INFO ->
|
||||||
process_iq_disco_info(From, Type, Lang, StateData);
|
process_iq_disco_info(From, Type, Lang, StateData);
|
||||||
?NS_DISCO_ITEMS ->
|
?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,
|
end,
|
||||||
{IQRes, NewStateData} =
|
{IQRes, NewStateData} =
|
||||||
case Res1 of
|
case Res1 of
|
||||||
@ -707,6 +711,30 @@ handle_info(process_room_queue, normal_state = StateName, StateData) ->
|
|||||||
{empty, _} ->
|
{empty, _} ->
|
||||||
{next_state, StateName, StateData}
|
{next_state, StateName, StateData}
|
||||||
end;
|
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) ->
|
handle_info(_Info, StateName, StateData) ->
|
||||||
{next_state, StateName, StateData}.
|
{next_state, StateName, StateData}.
|
||||||
|
|
||||||
@ -1512,8 +1540,8 @@ add_new_user(From, Nick, Packet, StateData) ->
|
|||||||
From, Err),
|
From, Err),
|
||||||
StateData;
|
StateData;
|
||||||
{_, _, _, Role} ->
|
{_, _, _, Role} ->
|
||||||
case check_password(ServiceAffiliation,
|
case check_password(ServiceAffiliation, Affiliation,
|
||||||
exmpp_xml:get_child_elements(Packet),
|
exmpp_xml:get_child_elements(Packet), From,
|
||||||
StateData) of
|
StateData) of
|
||||||
true ->
|
true ->
|
||||||
NewState =
|
NewState =
|
||||||
@ -1550,7 +1578,8 @@ add_new_user(From, Nick, Packet, StateData) ->
|
|||||||
true ->
|
true ->
|
||||||
NewState#state{just_created = false};
|
NewState#state{just_created = false};
|
||||||
false ->
|
false ->
|
||||||
NewState
|
Robots = ?DICT:erase(From, StateData#state.robots),
|
||||||
|
NewState#state{robots = Robots}
|
||||||
end;
|
end;
|
||||||
nopass ->
|
nopass ->
|
||||||
ErrText = "The password is required to enter this room",
|
ErrText = "The password is required to enter this room",
|
||||||
@ -1561,6 +1590,29 @@ add_new_user(From, Nick, Packet, StateData) ->
|
|||||||
StateData#state.jid, Nick),
|
StateData#state.jid, Nick),
|
||||||
From, Err),
|
From, Err),
|
||||||
StateData;
|
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",
|
ErrText = "Incorrect password",
|
||||||
Err = exmpp_stanza:reply_with_error(
|
Err = exmpp_stanza:reply_with_error(
|
||||||
@ -1573,13 +1625,13 @@ add_new_user(From, Nick, Packet, StateData) ->
|
|||||||
end
|
end
|
||||||
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)
|
%% Don't check pass if user is owner in MUC service (access_admin option)
|
||||||
true;
|
true;
|
||||||
check_password(_ServiceAffiliation, Els, StateData) ->
|
check_password(_ServiceAffiliation, Affiliation, Els, From, StateData) ->
|
||||||
case (StateData#state.config)#config.password_protected of
|
case (StateData#state.config)#config.password_protected of
|
||||||
false ->
|
false ->
|
||||||
true;
|
check_captcha(Affiliation, From, StateData);
|
||||||
true ->
|
true ->
|
||||||
Pass = extract_password(Els),
|
Pass = extract_password(Els),
|
||||||
case Pass of
|
case Pass of
|
||||||
@ -1590,11 +1642,25 @@ check_password(_ServiceAffiliation, Els, StateData) ->
|
|||||||
Pass ->
|
Pass ->
|
||||||
true;
|
true;
|
||||||
_ ->
|
_ ->
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
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([]) ->
|
extract_password([]) ->
|
||||||
false;
|
false;
|
||||||
extract_password([#xmlel{ns = XMLNS} = El | Els]) ->
|
extract_password([#xmlel{ns = XMLNS} = El | Els]) ->
|
||||||
@ -2758,8 +2824,9 @@ get_config(Lang, StateData, From) ->
|
|||||||
end,
|
end,
|
||||||
Res =
|
Res =
|
||||||
[#xmlel{name = 'title', children = [ #xmlcdata{cdata =
|
[#xmlel{name = 'title', children = [ #xmlcdata{cdata =
|
||||||
translate:translate(Lang, "Configuration for ") ++
|
io_lib:format(translate:translate(Lang, "Configuration of room ~s"),
|
||||||
exmpp_jid:jid_to_list(StateData#state.jid)}]},
|
[exmpp_jid:jid_to_list(StateData#state.jid)])
|
||||||
|
}]},
|
||||||
#xmlel{name = 'field', attrs = [?XMLATTR('type', <<"hidden">>),
|
#xmlel{name = 'field', attrs = [?XMLATTR('type', <<"hidden">>),
|
||||||
?XMLATTR('var', <<"FORM_TYPE">>)],
|
?XMLATTR('var', <<"FORM_TYPE">>)],
|
||||||
children = [#xmlel{name = 'value', children = [#xmlcdata{cdata =
|
children = [#xmlel{name = 'value', children = [#xmlcdata{cdata =
|
||||||
@ -2865,9 +2932,14 @@ get_config(Lang, StateData, From) ->
|
|||||||
?BOOLXFIELD("Allow visitors to change nickname",
|
?BOOLXFIELD("Allow visitors to change nickname",
|
||||||
"muc#roomconfig_allowvisitornickchange",
|
"muc#roomconfig_allowvisitornickchange",
|
||||||
Config#config.allow_visitor_nickchange)
|
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(
|
case mod_muc_log:check_access_log(
|
||||||
StateData#state.server_host, From) of
|
StateData#state.server_host, From) of
|
||||||
allow ->
|
allow ->
|
||||||
@ -2954,6 +3026,8 @@ set_xoption([{"members_by_default", [Val]} | Opts], Config) ->
|
|||||||
?SET_BOOL_XOPT(members_by_default, Val);
|
?SET_BOOL_XOPT(members_by_default, Val);
|
||||||
set_xoption([{"muc#roomconfig_membersonly", [Val]} | Opts], Config) ->
|
set_xoption([{"muc#roomconfig_membersonly", [Val]} | Opts], Config) ->
|
||||||
?SET_BOOL_XOPT(members_only, Val);
|
?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_xoption([{"muc#roomconfig_allowinvites", [Val]} | Opts], Config) ->
|
||||||
?SET_BOOL_XOPT(allow_user_invites, Val);
|
?SET_BOOL_XOPT(allow_user_invites, Val);
|
||||||
set_xoption([{"muc#roomconfig_passwordprotectedroom", [Val]} | Opts], Config) ->
|
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}};
|
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}};
|
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}};
|
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}};
|
password -> StateData#state{config = (StateData#state.config)#config{password = Val}};
|
||||||
anonymous -> StateData#state{config = (StateData#state.config)#config{anonymous = Val}};
|
anonymous -> StateData#state{config = (StateData#state.config)#config{anonymous = Val}};
|
||||||
logging -> StateData#state{config = (StateData#state.config)#config{logging = 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(members_only),
|
||||||
?MAKE_CONFIG_OPT(allow_user_invites),
|
?MAKE_CONFIG_OPT(allow_user_invites),
|
||||||
?MAKE_CONFIG_OPT(password_protected),
|
?MAKE_CONFIG_OPT(password_protected),
|
||||||
|
?MAKE_CONFIG_OPT(captcha_protected),
|
||||||
?MAKE_CONFIG_OPT(password),
|
?MAKE_CONFIG_OPT(password),
|
||||||
?MAKE_CONFIG_OPT(anonymous),
|
?MAKE_CONFIG_OPT(anonymous),
|
||||||
?MAKE_CONFIG_OPT(logging),
|
?MAKE_CONFIG_OPT(logging),
|
||||||
@ -3226,6 +3302,17 @@ process_iq_disco_items(From, get, _Lang, StateData) ->
|
|||||||
{error, 'forbidden'}
|
{error, 'forbidden'}
|
||||||
end.
|
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) ->
|
get_title(StateData) ->
|
||||||
case (StateData#state.config)#config.title of
|
case (StateData#state.config)#config.title of
|
||||||
"" ->
|
"" ->
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
public_list = true,
|
public_list = true,
|
||||||
persistent = false,
|
persistent = false,
|
||||||
moderated = true,
|
moderated = true,
|
||||||
|
captcha_protected = false,
|
||||||
members_by_default = true,
|
members_by_default = true,
|
||||||
members_only = false,
|
members_only = false,
|
||||||
allow_user_invites = false,
|
allow_user_invites = false,
|
||||||
@ -66,6 +67,7 @@
|
|||||||
jid,
|
jid,
|
||||||
config = #config{},
|
config = #config{},
|
||||||
users = ?DICT:new(),
|
users = ?DICT:new(),
|
||||||
|
robots = ?DICT:new(),
|
||||||
affiliations = ?DICT:new(),
|
affiliations = ?DICT:new(),
|
||||||
history,
|
history,
|
||||||
subject = "",
|
subject = "",
|
||||||
|
@ -114,6 +114,10 @@ start_link({SockMod, Socket}, Opts) ->
|
|||||||
{value, {request_handlers, H}} -> H;
|
{value, {request_handlers, H}} -> H;
|
||||||
false -> []
|
false -> []
|
||||||
end ++
|
end ++
|
||||||
|
case lists:member(captcha, Opts) of
|
||||||
|
true -> [{["captcha"], ejabberd_captcha}];
|
||||||
|
false -> []
|
||||||
|
end ++
|
||||||
case lists:member(web_admin, Opts) of
|
case lists:member(web_admin, Opts) of
|
||||||
true -> [{["admin"], ejabberd_web_admin}];
|
true -> [{["admin"], ejabberd_web_admin}];
|
||||||
false -> []
|
false -> []
|
||||||
|
21
tools/captcha.sh
Normal file
21
tools/captcha.sh
Normal file
@ -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:-
|
Loading…
Reference in New Issue
Block a user