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:
parent
252ee6228b
commit
07cf6f09b8
@ -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).
|
||||||
|
@ -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} ->
|
||||||
|
@ -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)),
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user