25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-24 16:23:40 +01:00

Implement CAPTCHA limit

This commit is contained in:
Evgeniy Khramtsov 2011-04-14 18:03:02 +10:00
parent 252ee6228b
commit 07cf6f09b8
5 changed files with 145 additions and 51 deletions

View File

@ -27,7 +27,7 @@
-module(ejabberd_captcha). -module(ejabberd_captcha).
-behaviour(gen_server). -behaviour(gen_server).
-compile(export_all).
%% API %% API
-export([start_link/0]). -export([start_link/0]).
@ -37,7 +37,7 @@
-export([create_captcha/6, build_captcha_html/2, check_captcha/2, -export([create_captcha/6, build_captcha_html/2, check_captcha/2,
process_reply/1, process/2, is_feature_available/0, process_reply/1, process/2, is_feature_available/0,
create_captcha_x/4, create_captcha_x/5]). create_captcha_x/5, create_captcha_x/6]).
-include("jlib.hrl"). -include("jlib.hrl").
-include("ejabberd.hrl"). -include("ejabberd.hrl").
@ -49,8 +49,9 @@
-define(CAPTCHA_TEXT(Lang), translate:translate(Lang, "Enter the text you see")). -define(CAPTCHA_TEXT(Lang), translate:translate(Lang, "Enter the text you see")).
-define(CAPTCHA_LIFETIME, 120000). % two minutes -define(CAPTCHA_LIFETIME, 120000). % two minutes
-define(LIMIT_PERIOD, 60*1000*1000). % one minute
-record(state, {}). -record(state, {limits = treap:empty()}).
-record(captcha, {id, pid, key, tref, args}). -record(captcha, {id, pid, key, tref, args}).
-define(T(S), -define(T(S),
@ -72,11 +73,12 @@
start_link() -> start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
create_captcha(Id, SID, From, To, Lang, Args) create_captcha(SID, From, To, Lang, Limiter, Args)
when is_list(Id), is_list(Lang), is_list(SID), when is_list(Lang), is_list(SID),
is_record(From, jid), is_record(To, jid) -> is_record(From, jid), is_record(To, jid) ->
case create_image() of case create_image(Limiter) of
{ok, Type, Key, Image} -> {ok, Type, Key, Image} ->
Id = randoms:get_string(),
B64Image = jlib:encode_base64(binary_to_list(Image)), B64Image = jlib:encode_base64(binary_to_list(Image)),
JID = jlib:jid_to_string(From), JID = jlib:jid_to_string(From),
CID = "sha1+" ++ sha:sha(Image) ++ "@bob.xmpp.org", CID = "sha1+" ++ sha:sha(Image) ++ "@bob.xmpp.org",
@ -106,19 +108,19 @@ create_captcha(Id, SID, From, To, Lang, Args)
case ?T(mnesia:write(#captcha{id=Id, pid=self(), key=Key, case ?T(mnesia:write(#captcha{id=Id, pid=self(), key=Key,
tref=Tref, args=Args})) of tref=Tref, args=Args})) of
ok -> ok ->
{ok, [Body, OOB, Captcha, Data]}; {ok, Id, [Body, OOB, Captcha, Data]};
_Err -> Err ->
error {error, Err}
end; end;
_Err -> Err ->
error Err
end. end.
create_captcha_x(SID, To, Lang, HeadEls) -> create_captcha_x(SID, To, Lang, Limiter, HeadEls) ->
create_captcha_x(SID, To, Lang, HeadEls, []). create_captcha_x(SID, To, Lang, Limiter, HeadEls, []).
create_captcha_x(SID, To, Lang, HeadEls, TailEls) -> create_captcha_x(SID, To, Lang, Limiter, HeadEls, TailEls) ->
case create_image() of case create_image(Limiter) of
{ok, Type, Key, Image} -> {ok, Type, Key, Image} ->
Id = randoms:get_string(), Id = randoms:get_string(),
B64Image = jlib:encode_base64(binary_to_list(Image)), B64Image = jlib:encode_base64(binary_to_list(Image)),
@ -156,11 +158,11 @@ create_captcha_x(SID, To, Lang, HeadEls, TailEls) ->
case ?T(mnesia:write(#captcha{id=Id, key=Key, tref=Tref})) of case ?T(mnesia:write(#captcha{id=Id, key=Key, tref=Tref})) of
ok -> ok ->
{ok, [Captcha, Data]}; {ok, [Captcha, Data]};
_Err -> Err ->
error {error, Err}
end; end;
_ -> Err ->
error Err
end. end.
%% @spec (Id::string(), Lang::string()) -> {FormEl, {ImgEl, TextEl, IdEl, KeyEl}} | captcha_not_found %% @spec (Id::string(), Lang::string()) -> {FormEl, {ImgEl, TextEl, IdEl, KeyEl}} | captcha_not_found
@ -275,16 +277,19 @@ process(_Handlers, #request{method='GET', lang=Lang, path=[_, Id]}) ->
ejabberd_web:error(not_found) ejabberd_web:error(not_found)
end; end;
process(_Handlers, #request{method='GET', path=[_, Id, "image"]}) -> process(_Handlers, #request{method='GET', path=[_, Id, "image"], ip = IP}) ->
{Addr, _Port} = IP,
case mnesia:dirty_read(captcha, Id) of case mnesia:dirty_read(captcha, Id) of
[#captcha{key=Key}] -> [#captcha{key=Key}] ->
case create_image(Key) of case create_image(Addr, Key) of
{ok, Type, _, Img} -> {ok, Type, _, Img} ->
{200, {200,
[{"Content-Type", Type}, [{"Content-Type", Type},
{"Cache-Control", "no-cache"}, {"Cache-Control", "no-cache"},
{"Last-Modified", httpd_util:rfc1123_date()}], {"Last-Modified", httpd_util:rfc1123_date()}],
Img}; Img};
{error, limit} ->
ejabberd_web:error(not_allowed);
_ -> _ ->
ejabberd_web:error(not_found) ejabberd_web:error(not_found)
end; end;
@ -323,6 +328,20 @@ init([]) ->
check_captcha_setup(), check_captcha_setup(),
{ok, #state{}}. {ok, #state{}}.
handle_call({is_limited, Limiter, RateLimit}, _From, State) ->
NowPriority = now_priority(),
CleanPriority = NowPriority + ?LIMIT_PERIOD,
Limits = clean_treap(State#state.limits, CleanPriority),
case treap:lookup(Limiter, Limits) of
{ok, _, Rate} when Rate >= RateLimit ->
{reply, true, State#state{limits = Limits}};
{ok, Priority, Rate} ->
NewLimits = treap:insert(Limiter, Priority, Rate+1, Limits),
{reply, false, State#state{limits = NewLimits}};
_ ->
NewLimits = treap:insert(Limiter, NowPriority, 1, Limits),
{reply, false, State#state{limits = NewLimits}}
end;
handle_call(_Request, _From, State) -> handle_call(_Request, _From, State) ->
{reply, bad_request, State}. {reply, bad_request, State}.
@ -364,11 +383,22 @@ code_change(_OldVsn, State, _Extra) ->
%% Reason = atom() %% Reason = atom()
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
create_image() -> create_image() ->
create_image(undefined).
create_image(Limiter) ->
%% Six numbers from 1 to 9. %% Six numbers from 1 to 9.
Key = string:substr(randoms:get_string(), 1, 6), Key = string:substr(randoms:get_string(), 1, 6),
create_image(Key). create_image(Limiter, Key).
create_image(Key) -> create_image(Limiter, Key) ->
case is_limited(Limiter) of
true ->
{error, limit};
false ->
do_create_image(Key)
end.
do_create_image(Key) ->
FileName = get_prog_name(), FileName = get_prog_name(),
Cmd = lists:flatten(io_lib:format("~s ~s", [FileName, Key])), Cmd = lists:flatten(io_lib:format("~s ~s", [FileName, Key])),
case cmd(Cmd) of case cmd(Cmd) of
@ -455,6 +485,25 @@ get_captcha_transfer_protocol([{{_Port, _Ip, tcp}, ejabberd_http, Opts}
get_captcha_transfer_protocol([_ | Listeners]) -> get_captcha_transfer_protocol([_ | Listeners]) ->
get_captcha_transfer_protocol(Listeners). get_captcha_transfer_protocol(Listeners).
is_limited(undefined) ->
false;
is_limited(Limiter) ->
case ejabberd_config:get_local_option(captcha_limit) of
Int when is_integer(Int), Int > 0 ->
case catch gen_server:call(?MODULE, {is_limited, Limiter, Int},
5000) of
true ->
true;
false ->
false;
Err ->
?ERROR_MSG("Call failed: ~p", [Err]),
false
end;
_ ->
false
end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Function: cmd(Cmd) -> Data | {error, Reason} %% Function: cmd(Cmd) -> Data | {error, Reason}
%% Cmd = string() %% Cmd = string()
@ -514,17 +563,41 @@ is_feature_available() ->
case is_feature_enabled() of case is_feature_enabled() of
false -> false; false -> false;
true -> true ->
case create_image() of %% Do not generate image in order to avoid CAPTCHA DoS
{ok, _, _, _} -> true; %% case create_image() of
_Error -> false %% {ok, _, _, _} -> true;
end %% _Error -> false
%% end
true
end. end.
check_captcha_setup() -> check_captcha_setup() ->
case is_feature_enabled() andalso not is_feature_available() of AbleToGenerateCaptcha = case create_image() of
{ok, _, _, _} -> true;
_Error -> false
end,
case is_feature_enabled() andalso not AbleToGenerateCaptcha of
true -> true ->
?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, " ?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, "
"but it can't generate images.", []); "but it can't generate images.", []);
false -> false ->
ok ok
end. end.
clean_treap(Treap, CleanPriority) ->
case treap:is_empty(Treap) of
true ->
Treap;
false ->
{_Key, Priority, _Value} = treap:get_root(Treap),
if
Priority > CleanPriority ->
clean_treap(treap:delete_root(Treap), CleanPriority);
true ->
Treap
end
end.
now_priority() ->
{MSec, Sec, USec} = now(),
-((MSec*1000000 + Sec)*1000000 + USec).

View File

@ -431,6 +431,8 @@ process_term(Term, State) ->
add_option(captcha_cmd, Cmd, State); add_option(captcha_cmd, Cmd, State);
{captcha_host, Host} -> {captcha_host, Host} ->
add_option(captcha_host, Host, State); add_option(captcha_host, Host, State);
{captcha_limit, Limit} ->
add_option(captcha_limit, Limit, State);
{ejabberdctl_access_commands, ACs} -> {ejabberdctl_access_commands, ACs} ->
add_option(ejabberdctl_access_commands, ACs, State); add_option(ejabberdctl_access_commands, ACs, State);
{loglevel, Loglevel} -> {loglevel, Loglevel} ->

View File

@ -1629,19 +1629,28 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
From, Err), From, Err),
StateData; StateData;
captcha_required -> captcha_required ->
ID = randoms:get_string(),
SID = xml:get_attr_s("id", Attrs), SID = xml:get_attr_s("id", Attrs),
RoomJID = StateData#state.jid, RoomJID = StateData#state.jid,
To = jlib:jid_replace_resource(RoomJID, Nick), To = jlib:jid_replace_resource(RoomJID, Nick),
Limiter = {From#jid.luser, From#jid.lserver},
case ejabberd_captcha:create_captcha( case ejabberd_captcha:create_captcha(
ID, SID, RoomJID, To, Lang, From) of SID, RoomJID, To, Lang, Limiter, From) of
{ok, CaptchaEls} -> {ok, ID, CaptchaEls} ->
MsgPkt = {xmlelement, "message", [{"id", ID}], CaptchaEls}, MsgPkt = {xmlelement, "message", [{"id", ID}], CaptchaEls},
Robots = ?DICT:store(From, Robots = ?DICT:store(From,
{Nick, Packet}, StateData#state.robots), {Nick, Packet}, StateData#state.robots),
ejabberd_router:route(RoomJID, From, MsgPkt), ejabberd_router:route(RoomJID, From, MsgPkt),
StateData#state{robots = Robots}; StateData#state{robots = Robots};
error -> {error, limit} ->
ErrText = "Too many CAPTCHA requests",
Err = jlib:make_error_reply(
Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)),
ejabberd_router:route( % TODO: s/Nick/""/
jlib:jid_replace_resource(
StateData#state.jid, Nick),
From, Err),
StateData;
_ ->
ErrText = "Unable to generate a captcha", ErrText = "Unable to generate a captcha",
Err = jlib:make_error_reply( Err = jlib:make_error_reply(
Packet, ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText)), Packet, ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText)),

View File

@ -234,13 +234,18 @@ process_iq(From, To,
{"var", "password"}], {"var", "password"}],
[{xmlelement, "required", [], []}]}, [{xmlelement, "required", [], []}]},
case ejabberd_captcha:create_captcha_x( case ejabberd_captcha:create_captcha_x(
ID, To, Lang, [InstrEl, UField, PField]) of ID, To, Lang, Source, [InstrEl, UField, PField]) of
{ok, CaptchaEls} -> {ok, CaptchaEls} ->
IQ#iq{type = result, IQ#iq{type = result,
sub_el = [{xmlelement, "query", sub_el = [{xmlelement, "query",
[{"xmlns", "jabber:iq:register"}], [{"xmlns", "jabber:iq:register"}],
[TopInstrEl | CaptchaEls]}]}; [TopInstrEl | CaptchaEls]}]};
error -> {error, limit} ->
ErrText = "Too many CAPTCHA requests",
IQ#iq{type = error,
sub_el = [SubEl, ?ERRT_RESOURCE_CONSTRAINT(
Lang, ErrText)]};
_Err ->
ErrText = "Unable to generate a CAPTCHA", ErrText = "Unable to generate a CAPTCHA",
IQ#iq{type = error, IQ#iq{type = error,
sub_el = [SubEl, ?ERRT_INTERNAL_SERVER_ERROR( sub_el = [SubEl, ?ERRT_INTERNAL_SERVER_ERROR(

View File

@ -86,8 +86,9 @@ process([], #request{method = 'GET', lang = Lang}) ->
process(["register.css"], #request{method = 'GET'}) -> process(["register.css"], #request{method = 'GET'}) ->
serve_css(); serve_css();
process(["new"], #request{method = 'GET', lang = Lang, host = Host}) -> process(["new"], #request{method = 'GET', lang = Lang, host = Host, ip = IP}) ->
form_new_get(Host, Lang); {Addr, _Port} = IP,
form_new_get(Host, Lang, Addr);
process(["delete"], #request{method = 'GET', lang = Lang, host = Host}) -> process(["delete"], #request{method = 'GET', lang = Lang, host = Host}) ->
form_del_get(Host, Lang); form_del_get(Host, Lang);
@ -185,8 +186,8 @@ index_page(Lang) ->
%%% Formulary new account GET %%% Formulary new account GET
%%%---------------------------------------------------------------------- %%%----------------------------------------------------------------------
form_new_get(Host, Lang) -> form_new_get(Host, Lang, IP) ->
CaptchaEls = build_captcha_li_list(Lang), CaptchaEls = build_captcha_li_list(Lang, IP),
HeadEls = [ HeadEls = [
?XCT("title", "Register a Jabber account"), ?XCT("title", "Register a Jabber account"),
?XA("link", ?XA("link",
@ -336,27 +337,31 @@ form_new_post(Username, Host, Password, {Id, Key}) ->
%%% Formulary Captcha support for new GET/POST %%% Formulary Captcha support for new GET/POST
%%%---------------------------------------------------------------------- %%%----------------------------------------------------------------------
build_captcha_li_list(Lang) -> build_captcha_li_list(Lang, IP) ->
case ejabberd_captcha:is_feature_available() of case ejabberd_captcha:is_feature_available() of
true -> build_captcha_li_list2(Lang); true -> build_captcha_li_list2(Lang, IP);
false -> [] false -> []
end. end.
build_captcha_li_list2(Lang) -> build_captcha_li_list2(Lang, IP) ->
Id = randoms:get_string(),
SID = "", SID = "",
From = #jid{user = "", server = "test", resource = ""}, From = #jid{user = "", server = "test", resource = ""},
To = #jid{user = "", server = "test", resource = ""}, To = #jid{user = "", server = "test", resource = ""},
Args = [], Args = [],
ejabberd_captcha:create_captcha(Id, SID, From, To, Lang, Args), case ejabberd_captcha:create_captcha(SID, From, To, Lang, IP, Args) of
{_, {CImg,CText,CId,CKey}} = ejabberd_captcha:build_captcha_html(Id, Lang), {ok, Id, _} ->
{_, {CImg,CText,CId,CKey}} =
ejabberd_captcha:build_captcha_html(Id, Lang),
[?XE("li", [CText, [?XE("li", [CText,
?C(" "), ?C(" "),
CId, CId,
CKey, CKey,
?BR, ?BR,
CImg] CImg]
)]. )];
_ ->
[]
end.
%%%---------------------------------------------------------------------- %%%----------------------------------------------------------------------
%%% Formulary change password GET %%% Formulary change password GET