2009-03-13 17:02:59 +01:00
|
|
|
%%%-------------------------------------------------------------------
|
|
|
|
%%% File : ejabberd_captcha.erl
|
|
|
|
%%% Author : Evgeniy Khramtsov <xramtsov@gmail.com>
|
2009-06-09 12:56:14 +02:00
|
|
|
%%% Purpose : CAPTCHA processing.
|
2009-03-13 17:02:59 +01:00
|
|
|
%%% Created : 26 Apr 2008 by Evgeniy Khramtsov <xramtsov@gmail.com>
|
2009-06-09 12:56:14 +02:00
|
|
|
%%%
|
|
|
|
%%%
|
2016-01-13 12:29:14 +01:00
|
|
|
%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
|
2009-06-09 12:56:14 +02:00
|
|
|
%%%
|
|
|
|
%%% This program is free software; you can redistribute it and/or
|
|
|
|
%%% modify it under the terms of the GNU General Public License as
|
|
|
|
%%% published by the Free Software Foundation; either version 2 of the
|
|
|
|
%%% License, or (at your option) any later version.
|
|
|
|
%%%
|
|
|
|
%%% This program is distributed in the hope that it will be useful,
|
|
|
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
%%% General Public License for more details.
|
|
|
|
%%%
|
2014-02-22 11:27:40 +01:00
|
|
|
%%% You should have received a copy of the GNU General Public License along
|
|
|
|
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
2009-06-09 12:56:14 +02:00
|
|
|
%%%
|
2009-03-13 17:02:59 +01:00
|
|
|
%%%-------------------------------------------------------------------
|
2009-06-09 12:56:14 +02:00
|
|
|
|
2009-03-13 17:02:59 +01:00
|
|
|
-module(ejabberd_captcha).
|
|
|
|
|
2015-06-01 14:38:27 +02:00
|
|
|
-behaviour(ejabberd_config).
|
|
|
|
|
2015-05-21 17:02:36 +02:00
|
|
|
-protocol({xep, 158, '1.0'}).
|
|
|
|
|
2009-03-13 17:02:59 +01:00
|
|
|
-behaviour(gen_server).
|
2011-04-18 08:09:05 +02:00
|
|
|
|
2009-03-13 17:02:59 +01:00
|
|
|
%% API
|
|
|
|
-export([start_link/0]).
|
|
|
|
|
|
|
|
%% gen_server callbacks
|
2013-03-14 10:33:02 +01:00
|
|
|
-export([init/1, handle_call/3, handle_cast/2,
|
|
|
|
handle_info/2, terminate/2, code_change/3]).
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-export([create_captcha/6, build_captcha_html/2,
|
|
|
|
check_captcha/2, process_reply/1, process/2,
|
|
|
|
is_feature_available/0, create_captcha_x/5,
|
2015-06-01 14:38:27 +02:00
|
|
|
create_captcha_x/6, opt_type/1]).
|
2009-03-13 17:02:59 +01:00
|
|
|
|
|
|
|
-include("jlib.hrl").
|
2013-03-14 10:33:02 +01:00
|
|
|
|
2009-03-13 17:02:59 +01:00
|
|
|
-include("ejabberd.hrl").
|
2013-04-08 11:12:54 +02:00
|
|
|
-include("logger.hrl").
|
2013-03-14 10:33:02 +01:00
|
|
|
|
2013-04-08 11:12:54 +02:00
|
|
|
-include("ejabberd_http.hrl").
|
2009-03-13 17:02:59 +01:00
|
|
|
|
|
|
|
-define(VFIELD(Type, Var, Value),
|
2013-03-14 10:33:02 +01:00
|
|
|
#xmlel{name = <<"field">>,
|
|
|
|
attrs = [{<<"type">>, Type}, {<<"var">>, Var}],
|
|
|
|
children =
|
|
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
|
|
children = [Value]}]}).
|
|
|
|
|
|
|
|
-define(CAPTCHA_TEXT(Lang),
|
|
|
|
translate:translate(Lang,
|
|
|
|
<<"Enter the text you see">>)).
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-define(CAPTCHA_LIFETIME, 120000).
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-define(LIMIT_PERIOD, 60*1000*1000).
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-type error() :: efbig | enodata | limit | malformed_image | timeout.
|
|
|
|
|
|
|
|
-record(state, {limits = treap:empty() :: treap:treap()}).
|
|
|
|
|
|
|
|
-record(captcha, {id :: binary(),
|
|
|
|
pid :: pid(),
|
|
|
|
key :: binary(),
|
|
|
|
tref :: reference(),
|
|
|
|
args :: any()}).
|
2009-03-14 07:27:05 +01:00
|
|
|
|
2009-03-13 17:02:59 +01:00
|
|
|
start_link() ->
|
2013-03-14 10:33:02 +01:00
|
|
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [],
|
|
|
|
[]).
|
|
|
|
|
|
|
|
-spec create_captcha(binary(), jid(), jid(),
|
|
|
|
binary(), any(), any()) -> {error, error()} |
|
|
|
|
{ok, binary(), [xmlel()]}.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
create_captcha(SID, From, To, Lang, Limiter, Args) ->
|
2011-04-14 10:03:02 +02:00
|
|
|
case create_image(Limiter) of
|
2013-03-14 10:33:02 +01:00
|
|
|
{ok, Type, Key, Image} ->
|
|
|
|
Id = <<(randoms:get_string())/binary>>,
|
|
|
|
B64Image = jlib:encode_base64((Image)),
|
2015-11-24 16:44:13 +01:00
|
|
|
JID = jid:to_string(From),
|
2013-06-20 10:40:44 +02:00
|
|
|
CID = <<"sha1+", (p1_sha:sha(Image))/binary,
|
2013-03-14 10:33:02 +01:00
|
|
|
"@bob.xmpp.org">>,
|
|
|
|
Data = #xmlel{name = <<"data">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"xmlns">>, ?NS_BOB}, {<<"cid">>, CID},
|
|
|
|
{<<"max-age">>, <<"0">>}, {<<"type">>, Type}],
|
|
|
|
children = [{xmlcdata, B64Image}]},
|
|
|
|
Captcha = #xmlel{name = <<"captcha">>,
|
|
|
|
attrs = [{<<"xmlns">>, ?NS_CAPTCHA}],
|
|
|
|
children =
|
|
|
|
[#xmlel{name = <<"x">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"xmlns">>, ?NS_XDATA},
|
|
|
|
{<<"type">>, <<"form">>}],
|
|
|
|
children =
|
|
|
|
[?VFIELD(<<"hidden">>,
|
|
|
|
<<"FORM_TYPE">>,
|
|
|
|
{xmlcdata, ?NS_CAPTCHA}),
|
|
|
|
?VFIELD(<<"hidden">>, <<"from">>,
|
|
|
|
{xmlcdata,
|
2015-11-24 16:44:13 +01:00
|
|
|
jid:to_string(To)}),
|
2013-03-14 10:33:02 +01:00
|
|
|
?VFIELD(<<"hidden">>,
|
|
|
|
<<"challenge">>,
|
|
|
|
{xmlcdata, Id}),
|
|
|
|
?VFIELD(<<"hidden">>, <<"sid">>,
|
|
|
|
{xmlcdata, SID}),
|
|
|
|
#xmlel{name = <<"field">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"var">>, <<"ocr">>},
|
|
|
|
{<<"label">>,
|
|
|
|
?CAPTCHA_TEXT(Lang)}],
|
|
|
|
children =
|
|
|
|
[#xmlel{name =
|
|
|
|
<<"required">>,
|
|
|
|
attrs = [],
|
|
|
|
children = []},
|
|
|
|
#xmlel{name =
|
|
|
|
<<"media">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"xmlns">>,
|
|
|
|
?NS_MEDIA}],
|
|
|
|
children =
|
|
|
|
[#xmlel{name
|
|
|
|
=
|
|
|
|
<<"uri">>,
|
|
|
|
attrs
|
|
|
|
=
|
|
|
|
[{<<"type">>,
|
|
|
|
Type}],
|
|
|
|
children
|
|
|
|
=
|
|
|
|
[{xmlcdata,
|
|
|
|
<<"cid:",
|
|
|
|
CID/binary>>}]}]}]}]}]},
|
|
|
|
BodyString1 = translate:translate(Lang,
|
|
|
|
<<"Your messages to ~s are being blocked. "
|
|
|
|
"To unblock them, visit ~s">>),
|
|
|
|
BodyString = iolist_to_binary(io_lib:format(BodyString1,
|
|
|
|
[JID, get_url(Id)])),
|
|
|
|
Body = #xmlel{name = <<"body">>, attrs = [],
|
|
|
|
children = [{xmlcdata, BodyString}]},
|
|
|
|
OOB = #xmlel{name = <<"x">>,
|
|
|
|
attrs = [{<<"xmlns">>, ?NS_OOB}],
|
|
|
|
children =
|
|
|
|
[#xmlel{name = <<"url">>, attrs = [],
|
|
|
|
children = [{xmlcdata, get_url(Id)}]}]},
|
|
|
|
Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE,
|
|
|
|
{remove_id, Id}),
|
|
|
|
ets:insert(captcha,
|
|
|
|
#captcha{id = Id, pid = self(), key = Key, tref = Tref,
|
|
|
|
args = Args}),
|
|
|
|
{ok, Id, [Body, OOB, Captcha, Data]};
|
|
|
|
Err -> Err
|
2009-03-14 07:27:05 +01:00
|
|
|
end.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-spec create_captcha_x(binary(), jid(), binary(),
|
|
|
|
any(), [xmlel()]) -> {ok, [xmlel()]} |
|
|
|
|
{error, error()}.
|
|
|
|
|
2011-04-14 10:03:02 +02:00
|
|
|
create_captcha_x(SID, To, Lang, Limiter, HeadEls) ->
|
|
|
|
create_captcha_x(SID, To, Lang, Limiter, HeadEls, []).
|
2010-10-24 07:30:16 +02:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-spec create_captcha_x(binary(), jid(), binary(),
|
|
|
|
any(), [xmlel()], [xmlel()]) -> {ok, [xmlel()]} |
|
|
|
|
{error, error()}.
|
|
|
|
|
|
|
|
create_captcha_x(SID, To, Lang, Limiter, HeadEls,
|
|
|
|
TailEls) ->
|
2011-04-14 10:03:02 +02:00
|
|
|
case create_image(Limiter) of
|
2013-03-14 10:33:02 +01:00
|
|
|
{ok, Type, Key, Image} ->
|
|
|
|
Id = <<(randoms:get_string())/binary>>,
|
|
|
|
B64Image = jlib:encode_base64((Image)),
|
2013-06-20 10:40:44 +02:00
|
|
|
CID = <<"sha1+", (p1_sha:sha(Image))/binary,
|
2013-03-14 10:33:02 +01:00
|
|
|
"@bob.xmpp.org">>,
|
|
|
|
Data = #xmlel{name = <<"data">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"xmlns">>, ?NS_BOB}, {<<"cid">>, CID},
|
|
|
|
{<<"max-age">>, <<"0">>}, {<<"type">>, Type}],
|
|
|
|
children = [{xmlcdata, B64Image}]},
|
|
|
|
HelpTxt = translate:translate(Lang,
|
|
|
|
<<"If you don't see the CAPTCHA image here, "
|
|
|
|
"visit the web page.">>),
|
|
|
|
Imageurl = get_url(<<Id/binary, "/image">>),
|
|
|
|
Captcha = #xmlel{name = <<"x">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"xmlns">>, ?NS_XDATA},
|
|
|
|
{<<"type">>, <<"form">>}],
|
|
|
|
children =
|
|
|
|
[?VFIELD(<<"hidden">>, <<"FORM_TYPE">>,
|
|
|
|
{xmlcdata, ?NS_CAPTCHA})
|
|
|
|
| HeadEls]
|
|
|
|
++
|
|
|
|
[#xmlel{name = <<"field">>,
|
|
|
|
attrs = [{<<"type">>, <<"fixed">>}],
|
|
|
|
children =
|
|
|
|
[#xmlel{name = <<"value">>,
|
|
|
|
attrs = [],
|
|
|
|
children =
|
|
|
|
[{xmlcdata,
|
|
|
|
HelpTxt}]}]},
|
|
|
|
#xmlel{name = <<"field">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"type">>, <<"hidden">>},
|
|
|
|
{<<"var">>, <<"captchahidden">>}],
|
|
|
|
children =
|
|
|
|
[#xmlel{name = <<"value">>,
|
|
|
|
attrs = [],
|
|
|
|
children =
|
|
|
|
[{xmlcdata,
|
|
|
|
<<"workaround-for-psi">>}]}]},
|
|
|
|
#xmlel{name = <<"field">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"type">>, <<"text-single">>},
|
|
|
|
{<<"label">>,
|
|
|
|
translate:translate(Lang,
|
|
|
|
<<"CAPTCHA web page">>)},
|
|
|
|
{<<"var">>, <<"url">>}],
|
|
|
|
children =
|
|
|
|
[#xmlel{name = <<"value">>,
|
|
|
|
attrs = [],
|
|
|
|
children =
|
|
|
|
[{xmlcdata,
|
|
|
|
Imageurl}]}]},
|
|
|
|
?VFIELD(<<"hidden">>, <<"from">>,
|
2015-11-24 16:44:13 +01:00
|
|
|
{xmlcdata, jid:to_string(To)}),
|
2013-03-14 10:33:02 +01:00
|
|
|
?VFIELD(<<"hidden">>, <<"challenge">>,
|
|
|
|
{xmlcdata, Id}),
|
|
|
|
?VFIELD(<<"hidden">>, <<"sid">>,
|
|
|
|
{xmlcdata, SID}),
|
|
|
|
#xmlel{name = <<"field">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"var">>, <<"ocr">>},
|
|
|
|
{<<"label">>,
|
|
|
|
?CAPTCHA_TEXT(Lang)}],
|
|
|
|
children =
|
|
|
|
[#xmlel{name = <<"required">>,
|
|
|
|
attrs = [], children = []},
|
|
|
|
#xmlel{name = <<"media">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"xmlns">>,
|
|
|
|
?NS_MEDIA}],
|
|
|
|
children =
|
|
|
|
[#xmlel{name =
|
|
|
|
<<"uri">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"type">>,
|
|
|
|
Type}],
|
|
|
|
children =
|
|
|
|
[{xmlcdata,
|
|
|
|
<<"cid:",
|
|
|
|
CID/binary>>}]}]}]}]
|
|
|
|
++ TailEls},
|
|
|
|
Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE,
|
|
|
|
{remove_id, Id}),
|
|
|
|
ets:insert(captcha,
|
|
|
|
#captcha{id = Id, key = Key, tref = Tref}),
|
|
|
|
{ok, [Captcha, Data]};
|
|
|
|
Err -> Err
|
2010-10-24 07:30:16 +02:00
|
|
|
end.
|
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-spec build_captcha_html(binary(), binary()) -> captcha_not_found |
|
|
|
|
{xmlel(),
|
|
|
|
{xmlel(), xmlel(),
|
|
|
|
xmlel(), xmlel()}}.
|
|
|
|
|
2009-03-30 13:55:31 +02:00
|
|
|
build_captcha_html(Id, Lang) ->
|
2013-03-14 10:33:02 +01:00
|
|
|
case lookup_captcha(Id) of
|
|
|
|
{ok, _} ->
|
|
|
|
ImgEl = #xmlel{name = <<"img">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"src">>, get_url(<<Id/binary, "/image">>)}],
|
|
|
|
children = []},
|
|
|
|
TextEl = {xmlcdata, ?CAPTCHA_TEXT(Lang)},
|
|
|
|
IdEl = #xmlel{name = <<"input">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"type">>, <<"hidden">>}, {<<"name">>, <<"id">>},
|
|
|
|
{<<"value">>, Id}],
|
|
|
|
children = []},
|
|
|
|
KeyEl = #xmlel{name = <<"input">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"type">>, <<"text">>}, {<<"name">>, <<"key">>},
|
|
|
|
{<<"size">>, <<"10">>}],
|
|
|
|
children = []},
|
|
|
|
FormEl = #xmlel{name = <<"form">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"action">>, get_url(Id)},
|
|
|
|
{<<"name">>, <<"captcha">>},
|
|
|
|
{<<"method">>, <<"POST">>}],
|
|
|
|
children =
|
|
|
|
[ImgEl,
|
|
|
|
#xmlel{name = <<"br">>, attrs = [],
|
|
|
|
children = []},
|
|
|
|
TextEl,
|
|
|
|
#xmlel{name = <<"br">>, attrs = [],
|
|
|
|
children = []},
|
|
|
|
IdEl, KeyEl,
|
|
|
|
#xmlel{name = <<"br">>, attrs = [],
|
|
|
|
children = []},
|
|
|
|
#xmlel{name = <<"input">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"type">>, <<"submit">>},
|
|
|
|
{<<"name">>, <<"enter">>},
|
|
|
|
{<<"value">>, <<"OK">>}],
|
|
|
|
children = []}]},
|
|
|
|
{FormEl, {ImgEl, TextEl, IdEl, KeyEl}};
|
|
|
|
_ -> captcha_not_found
|
2009-03-30 13:55:31 +02:00
|
|
|
end.
|
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-spec process_reply(xmlel()) -> ok | {error, bad_match | not_found | malformed}.
|
|
|
|
|
|
|
|
process_reply(#xmlel{} = El) ->
|
2016-02-03 19:03:17 +01:00
|
|
|
case fxml:get_subtag(El, <<"x">>) of
|
2013-03-14 10:33:02 +01:00
|
|
|
false -> {error, malformed};
|
|
|
|
Xdata ->
|
|
|
|
Fields = jlib:parse_xdata_submit(Xdata),
|
|
|
|
case catch {proplists:get_value(<<"challenge">>,
|
|
|
|
Fields),
|
|
|
|
proplists:get_value(<<"ocr">>, Fields)}
|
|
|
|
of
|
|
|
|
{[Id | _], [OCR | _]} ->
|
|
|
|
case check_captcha(Id, OCR) of
|
|
|
|
captcha_valid -> ok;
|
|
|
|
captcha_non_valid -> {error, bad_match};
|
|
|
|
captcha_not_found -> {error, not_found}
|
|
|
|
end;
|
|
|
|
_ -> {error, malformed}
|
|
|
|
end
|
2009-03-14 07:27:05 +01:00
|
|
|
end;
|
2013-03-14 10:33:02 +01:00
|
|
|
process_reply(_) -> {error, malformed}.
|
2009-03-30 13:55:31 +02:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
process(_Handlers,
|
|
|
|
#request{method = 'GET', lang = Lang,
|
|
|
|
path = [_, Id]}) ->
|
2009-03-30 13:55:31 +02:00
|
|
|
case build_captcha_html(Id, Lang) of
|
2013-03-14 10:33:02 +01:00
|
|
|
{FormEl, _} when is_tuple(FormEl) ->
|
|
|
|
Form = #xmlel{name = <<"div">>,
|
|
|
|
attrs = [{<<"align">>, <<"center">>}],
|
|
|
|
children = [FormEl]},
|
|
|
|
ejabberd_web:make_xhtml([Form]);
|
|
|
|
captcha_not_found -> ejabberd_web:error(not_found)
|
2009-03-14 07:27:05 +01:00
|
|
|
end;
|
2013-03-14 10:33:02 +01:00
|
|
|
process(_Handlers,
|
|
|
|
#request{method = 'GET', path = [_, Id, <<"image">>],
|
|
|
|
ip = IP}) ->
|
2011-04-14 10:03:02 +02:00
|
|
|
{Addr, _Port} = IP,
|
2013-03-14 10:33:02 +01:00
|
|
|
case lookup_captcha(Id) of
|
|
|
|
{ok, #captcha{key = Key}} ->
|
|
|
|
case create_image(Addr, Key) of
|
|
|
|
{ok, Type, _, Img} ->
|
|
|
|
{200,
|
|
|
|
[{<<"Content-Type">>, Type},
|
|
|
|
{<<"Cache-Control">>, <<"no-cache">>},
|
|
|
|
{<<"Last-Modified">>, list_to_binary(httpd_util:rfc1123_date())}],
|
|
|
|
Img};
|
|
|
|
{error, limit} -> ejabberd_web:error(not_allowed);
|
|
|
|
_ -> ejabberd_web:error(not_found)
|
|
|
|
end;
|
|
|
|
_ -> ejabberd_web:error(not_found)
|
2009-03-14 07:27:05 +01:00
|
|
|
end;
|
2013-03-14 10:33:02 +01:00
|
|
|
process(_Handlers,
|
|
|
|
#request{method = 'POST', q = Q, lang = Lang,
|
|
|
|
path = [_, Id]}) ->
|
|
|
|
ProvidedKey = proplists:get_value(<<"key">>, Q, none),
|
2009-03-30 13:55:31 +02:00
|
|
|
case check_captcha(Id, ProvidedKey) of
|
2013-03-14 10:33:02 +01:00
|
|
|
captcha_valid ->
|
|
|
|
Form = #xmlel{name = <<"p">>, attrs = [],
|
|
|
|
children =
|
|
|
|
[{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)
|
2009-07-30 15:08:26 +02:00
|
|
|
end;
|
|
|
|
process(_Handlers, _Request) ->
|
|
|
|
ejabberd_web:error(not_found).
|
2009-03-30 13:55:31 +02:00
|
|
|
|
2009-03-13 17:02:59 +01:00
|
|
|
init([]) ->
|
2013-03-14 10:33:02 +01:00
|
|
|
mnesia:delete_table(captcha),
|
|
|
|
ets:new(captcha,
|
|
|
|
[named_table, public, {keypos, #captcha.id}]),
|
2009-05-26 13:53:58 +02:00
|
|
|
check_captcha_setup(),
|
2009-03-13 17:02:59 +01:00
|
|
|
{ok, #state{}}.
|
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
handle_call({is_limited, Limiter, RateLimit}, _From,
|
|
|
|
State) ->
|
2011-04-14 10:03:02 +02:00
|
|
|
NowPriority = now_priority(),
|
2013-03-14 10:33:02 +01:00
|
|
|
CleanPriority = NowPriority + (?LIMIT_PERIOD),
|
2011-04-14 10:03:02 +02:00
|
|
|
Limits = clean_treap(State#state.limits, CleanPriority),
|
|
|
|
case treap:lookup(Limiter, Limits) of
|
2013-03-14 10:33:02 +01:00
|
|
|
{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}}
|
2011-04-14 10:03:02 +02:00
|
|
|
end;
|
2009-03-13 17:02:59 +01:00
|
|
|
handle_call(_Request, _From, State) ->
|
|
|
|
{reply, bad_request, State}.
|
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
handle_cast(_Msg, State) -> {noreply, State}.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
|
|
|
handle_info({remove_id, Id}, State) ->
|
2009-03-14 07:27:05 +01:00
|
|
|
?DEBUG("captcha ~p timed out", [Id]),
|
2013-03-14 10:33:02 +01:00
|
|
|
case ets:lookup(captcha, Id) of
|
|
|
|
[#captcha{args = Args, pid = Pid}] ->
|
|
|
|
if is_pid(Pid) -> Pid ! {captcha_failed, Args};
|
|
|
|
true -> ok
|
|
|
|
end,
|
|
|
|
ets:delete(captcha, Id);
|
|
|
|
_ -> ok
|
|
|
|
end,
|
2009-03-13 17:02:59 +01:00
|
|
|
{noreply, State};
|
2013-03-14 10:33:02 +01:00
|
|
|
handle_info(_Info, State) -> {noreply, State}.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
terminate(_Reason, _State) -> ok.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
code_change(_OldVsn, State, _Extra) -> {ok, State}.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
create_image() -> create_image(undefined).
|
2011-04-14 10:03:02 +02:00
|
|
|
|
|
|
|
create_image(Limiter) ->
|
2013-03-14 10:33:02 +01:00
|
|
|
Key = str:substr(randoms:get_string(), 1, 6),
|
2011-04-14 10:03:02 +02:00
|
|
|
create_image(Limiter, Key).
|
|
|
|
|
|
|
|
create_image(Limiter, Key) ->
|
|
|
|
case is_limited(Limiter) of
|
2013-03-14 10:33:02 +01:00
|
|
|
true -> {error, limit};
|
|
|
|
false -> do_create_image(Key)
|
2011-04-14 10:03:02 +02:00
|
|
|
end.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2011-04-14 10:03:02 +02:00
|
|
|
do_create_image(Key) ->
|
2009-03-13 17:02:59 +01:00
|
|
|
FileName = get_prog_name(),
|
|
|
|
Cmd = lists:flatten(io_lib:format("~s ~s", [FileName, Key])),
|
|
|
|
case cmd(Cmd) of
|
2013-03-14 10:33:02 +01:00
|
|
|
{ok,
|
|
|
|
<<137, $P, $N, $G, $\r, $\n, 26, $\n, _/binary>> =
|
|
|
|
Img} ->
|
|
|
|
{ok, <<"image/png">>, Key, Img};
|
|
|
|
{ok, <<255, 216, _/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}
|
2009-03-13 17:02:59 +01:00
|
|
|
end.
|
|
|
|
|
|
|
|
get_prog_name() ->
|
2013-08-12 14:25:05 +02:00
|
|
|
case ejabberd_config:get_option(
|
2013-03-14 10:33:02 +01:00
|
|
|
captcha_cmd,
|
|
|
|
fun(FileName) ->
|
|
|
|
F = iolist_to_binary(FileName),
|
|
|
|
if F /= <<"">> -> F end
|
|
|
|
end) of
|
|
|
|
undefined ->
|
|
|
|
?DEBUG("The option captcha_cmd is not configured, "
|
|
|
|
"but some module wants to use the CAPTCHA "
|
|
|
|
"feature.",
|
|
|
|
[]),
|
|
|
|
false;
|
|
|
|
FileName ->
|
|
|
|
FileName
|
2009-03-13 17:02:59 +01:00
|
|
|
end.
|
|
|
|
|
|
|
|
get_url(Str) ->
|
2013-08-12 14:25:05 +02:00
|
|
|
CaptchaHost = ejabberd_config:get_option(
|
2013-03-14 10:33:02 +01:00
|
|
|
captcha_host,
|
|
|
|
fun iolist_to_binary/1,
|
|
|
|
<<"">>),
|
|
|
|
case str:tokens(CaptchaHost, <<":">>) of
|
|
|
|
[Host] ->
|
|
|
|
<<"http://", Host/binary, "/captcha/", Str/binary>>;
|
|
|
|
[<<"http", _/binary>> = TransferProt, Host] ->
|
|
|
|
<<TransferProt/binary, ":", Host/binary, "/captcha/",
|
|
|
|
Str/binary>>;
|
|
|
|
[Host, PortString] ->
|
|
|
|
TransferProt =
|
|
|
|
iolist_to_binary(atom_to_list(get_transfer_protocol(PortString))),
|
|
|
|
<<TransferProt/binary, "://", Host/binary, ":",
|
|
|
|
PortString/binary, "/captcha/", Str/binary>>;
|
|
|
|
[TransferProt, Host, PortString] ->
|
|
|
|
<<TransferProt/binary, ":", Host/binary, ":",
|
|
|
|
PortString/binary, "/captcha/", Str/binary>>;
|
|
|
|
_ ->
|
|
|
|
<<"http://", (?MYNAME)/binary, "/captcha/", Str/binary>>
|
2009-03-13 17:02:59 +01:00
|
|
|
end.
|
|
|
|
|
2011-03-03 11:35:47 +01:00
|
|
|
get_transfer_protocol(PortString) ->
|
2013-03-14 10:33:02 +01:00
|
|
|
PortNumber = jlib:binary_to_integer(PortString),
|
2011-02-14 12:58:33 +01:00
|
|
|
PortListeners = get_port_listeners(PortNumber),
|
|
|
|
get_captcha_transfer_protocol(PortListeners).
|
|
|
|
|
|
|
|
get_port_listeners(PortNumber) ->
|
2013-08-12 14:25:05 +02:00
|
|
|
AllListeners = ejabberd_config:get_option(listen, fun(V) -> V end),
|
2014-12-24 00:35:22 +01:00
|
|
|
lists:filter(fun (Listener) when is_list(Listener) ->
|
|
|
|
case proplists:get_value(port, Listener) of
|
|
|
|
PortNumber -> true;
|
|
|
|
_ -> false
|
|
|
|
end;
|
2013-03-14 10:33:02 +01:00
|
|
|
(_) -> false
|
|
|
|
end,
|
|
|
|
AllListeners).
|
2011-02-14 12:58:33 +01:00
|
|
|
|
|
|
|
get_captcha_transfer_protocol([]) ->
|
2013-03-14 10:33:02 +01:00
|
|
|
throw(<<"The port number mentioned in captcha_host "
|
|
|
|
"is not a ejabberd_http listener with "
|
|
|
|
"'captcha' option. Change the port number "
|
|
|
|
"or specify http:// in that option.">>);
|
2014-12-24 00:35:22 +01:00
|
|
|
get_captcha_transfer_protocol([Listener | Listeners]) when is_list(Listener) ->
|
|
|
|
case proplists:get_value(module, Listener) == ejabberd_http andalso
|
|
|
|
proplists:get_bool(captcha, Listener) of
|
2013-03-14 10:33:02 +01:00
|
|
|
true ->
|
2014-12-24 00:35:22 +01:00
|
|
|
case proplists:get_bool(tls, Listener) of
|
2013-03-14 10:33:02 +01:00
|
|
|
true -> https;
|
|
|
|
false -> http
|
|
|
|
end;
|
|
|
|
false -> get_captcha_transfer_protocol(Listeners)
|
2011-02-14 12:58:33 +01:00
|
|
|
end;
|
|
|
|
get_captcha_transfer_protocol([_ | Listeners]) ->
|
|
|
|
get_captcha_transfer_protocol(Listeners).
|
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
is_limited(undefined) -> false;
|
2011-04-14 10:03:02 +02:00
|
|
|
is_limited(Limiter) ->
|
2013-08-12 14:25:05 +02:00
|
|
|
case ejabberd_config:get_option(
|
2013-03-14 10:33:02 +01:00
|
|
|
captcha_limit,
|
|
|
|
fun(I) when is_integer(I), I > 0 -> I end) of
|
|
|
|
undefined -> false;
|
|
|
|
Int ->
|
|
|
|
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
|
2011-04-14 10:03:02 +02:00
|
|
|
end.
|
|
|
|
|
2009-03-13 17:02:59 +01:00
|
|
|
-define(CMD_TIMEOUT, 5000).
|
2013-03-14 10:33:02 +01:00
|
|
|
|
|
|
|
-define(MAX_FILE_SIZE, 64 * 1024).
|
2009-03-13 17:02:59 +01:00
|
|
|
|
|
|
|
cmd(Cmd) ->
|
|
|
|
Port = open_port({spawn, Cmd}, [stream, eof, binary]),
|
2013-03-14 10:33:02 +01:00
|
|
|
TRef = erlang:start_timer(?CMD_TIMEOUT, self(),
|
|
|
|
timeout),
|
2009-03-13 17:02:59 +01:00
|
|
|
recv_data(Port, TRef, <<>>).
|
|
|
|
|
|
|
|
recv_data(Port, TRef, Buf) ->
|
|
|
|
receive
|
2013-03-14 10:33:02 +01:00
|
|
|
{Port, {data, Bytes}} ->
|
|
|
|
NewBuf = <<Buf/binary, Bytes/binary>>,
|
|
|
|
if byte_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})
|
2009-03-13 17:02:59 +01:00
|
|
|
end.
|
|
|
|
|
|
|
|
return(Port, TRef, Result) ->
|
|
|
|
case erlang:cancel_timer(TRef) of
|
2013-03-14 10:33:02 +01:00
|
|
|
false ->
|
|
|
|
receive {timeout, TRef, _} -> ok after 0 -> ok end;
|
|
|
|
_ -> ok
|
2009-03-13 17:02:59 +01:00
|
|
|
end,
|
|
|
|
catch port_close(Port),
|
|
|
|
Result.
|
2009-05-26 13:53:58 +02:00
|
|
|
|
|
|
|
is_feature_available() ->
|
2011-04-26 13:59:08 +02:00
|
|
|
case get_prog_name() of
|
2013-03-14 10:33:02 +01:00
|
|
|
Prog when is_binary(Prog) -> true;
|
|
|
|
false -> false
|
2009-05-26 13:53:58 +02:00
|
|
|
end.
|
|
|
|
|
|
|
|
check_captcha_setup() ->
|
2011-05-18 04:48:02 +02:00
|
|
|
case is_feature_available() of
|
2013-03-14 10:33:02 +01:00
|
|
|
true ->
|
|
|
|
case create_image() of
|
|
|
|
{ok, _, _, _} -> ok;
|
|
|
|
_Err ->
|
|
|
|
?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, "
|
|
|
|
"but it can't generate images.",
|
|
|
|
[]),
|
|
|
|
throw({error, captcha_cmd_enabled_but_fails})
|
|
|
|
end;
|
|
|
|
false -> ok
|
|
|
|
end.
|
|
|
|
|
|
|
|
lookup_captcha(Id) ->
|
|
|
|
case ets:lookup(captcha, Id) of
|
|
|
|
[C] -> {ok, C};
|
|
|
|
_ -> {error, enoent}
|
|
|
|
end.
|
|
|
|
|
2015-10-07 00:06:58 +02:00
|
|
|
-spec check_captcha(binary(), binary()) -> captcha_not_found |
|
|
|
|
captcha_valid |
|
|
|
|
captcha_non_valid.
|
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
check_captcha(Id, ProvidedKey) ->
|
|
|
|
case ets:lookup(captcha, Id) of
|
|
|
|
[#captcha{pid = Pid, args = Args, key = ValidKey,
|
|
|
|
tref = Tref}] ->
|
|
|
|
ets:delete(captcha, Id),
|
|
|
|
erlang:cancel_timer(Tref),
|
|
|
|
if ValidKey == ProvidedKey ->
|
|
|
|
if is_pid(Pid) -> Pid ! {captcha_succeed, Args};
|
|
|
|
true -> ok
|
|
|
|
end,
|
|
|
|
captcha_valid;
|
|
|
|
true ->
|
|
|
|
if is_pid(Pid) -> Pid ! {captcha_failed, Args};
|
|
|
|
true -> ok
|
|
|
|
end,
|
|
|
|
captcha_non_valid
|
|
|
|
end;
|
|
|
|
_ -> captcha_not_found
|
2009-05-26 13:53:58 +02:00
|
|
|
end.
|
2011-04-14 10:03:02 +02:00
|
|
|
|
|
|
|
clean_treap(Treap, CleanPriority) ->
|
|
|
|
case treap:is_empty(Treap) of
|
2013-03-14 10:33:02 +01:00
|
|
|
true -> Treap;
|
|
|
|
false ->
|
|
|
|
{_Key, Priority, _Value} = treap:get_root(Treap),
|
|
|
|
if Priority > CleanPriority ->
|
|
|
|
clean_treap(treap:delete_root(Treap), CleanPriority);
|
|
|
|
true -> Treap
|
|
|
|
end
|
2011-04-14 10:03:02 +02:00
|
|
|
end.
|
|
|
|
|
|
|
|
now_priority() ->
|
2015-12-04 15:08:43 +01:00
|
|
|
-p1_time_compat:system_time(micro_seconds).
|
2015-06-01 14:38:27 +02:00
|
|
|
|
|
|
|
opt_type(captcha_cmd) ->
|
|
|
|
fun (FileName) ->
|
|
|
|
F = iolist_to_binary(FileName), if F /= <<"">> -> F end
|
|
|
|
end;
|
|
|
|
opt_type(captcha_host) -> fun iolist_to_binary/1;
|
|
|
|
opt_type(captcha_limit) ->
|
|
|
|
fun (I) when is_integer(I), I > 0 -> I end;
|
|
|
|
opt_type(listen) -> fun (V) -> V end;
|
|
|
|
opt_type(_) ->
|
|
|
|
[captcha_cmd, captcha_host, captcha_limit, listen].
|