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
|
|
|
%%%
|
|
|
|
%%%
|
2024-01-22 16:40:01 +01:00
|
|
|
%%% ejabberd, Copyright (C) 2002-2024 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-05-21 17:02:36 +02:00
|
|
|
-protocol({xep, 158, '1.0'}).
|
2024-05-17 13:00:45 +02:00
|
|
|
-protocol({xep, 231, '1.0'}).
|
2015-05-21 17:02:36 +02:00
|
|
|
|
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,
|
2019-06-14 11:33:26 +02:00
|
|
|
host_up/1, host_down/1,
|
2018-01-26 13:02:06 +01:00
|
|
|
config_reloaded/0, process_iq/1]).
|
2013-03-14 10:33:02 +01:00
|
|
|
|
2020-09-03 13:45:57 +02:00
|
|
|
-include_lib("xmpp/include/xmpp.hrl").
|
2013-04-08 11:12:54 +02:00
|
|
|
-include("logger.hrl").
|
|
|
|
-include("ejabberd_http.hrl").
|
2019-06-22 16:08:45 +02:00
|
|
|
-include("translate.hrl").
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-define(CAPTCHA_LIFETIME, 120000).
|
|
|
|
-define(LIMIT_PERIOD, 60*1000*1000).
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2016-07-28 14:10:41 +02:00
|
|
|
-type image_error() :: efbig | enodata | limit | malformed_image | timeout.
|
2019-06-14 11:33:26 +02:00
|
|
|
-type priority() :: neg_integer().
|
2019-07-10 09:31:51 +02:00
|
|
|
-type callback() :: fun((captcha_succeed | captcha_failed) -> any()).
|
2013-03-14 10:33:02 +01:00
|
|
|
|
2018-01-26 13:02:06 +01:00
|
|
|
-record(state, {limits = treap:empty() :: treap:treap(),
|
|
|
|
enabled = false :: boolean()}).
|
2013-03-14 10:33:02 +01:00
|
|
|
|
|
|
|
-record(captcha, {id :: binary(),
|
2017-02-16 12:18:36 +01:00
|
|
|
pid :: pid() | undefined,
|
2013-03-14 10:33:02 +01:00
|
|
|
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, [],
|
|
|
|
[]).
|
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec captcha_text(binary()) -> binary().
|
2016-07-28 14:10:41 +02:00
|
|
|
captcha_text(Lang) ->
|
2019-06-22 16:08:45 +02:00
|
|
|
translate:translate(Lang, ?T("Enter the text you see")).
|
2016-07-28 14:10:41 +02:00
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec mk_ocr_field(binary(), binary(), binary()) -> xdata_field().
|
2016-07-28 14:10:41 +02:00
|
|
|
mk_ocr_field(Lang, CID, Type) ->
|
|
|
|
URI = #media_uri{type = Type, uri = <<"cid:", CID/binary>>},
|
2019-07-16 16:51:51 +02:00
|
|
|
[_, F] = captcha_form:encode([{ocr, <<>>}], Lang, [ocr]),
|
|
|
|
xmpp:set_els(F, [#media{uri = [URI]}]).
|
2016-07-28 14:10:41 +02:00
|
|
|
|
2022-12-26 16:06:23 +01:00
|
|
|
update_captcha_key(_Id, Key, Key) ->
|
|
|
|
ok;
|
|
|
|
update_captcha_key(Id, _Key, Key2) ->
|
|
|
|
true = ets:update_element(captcha, Id, [{4, Key2}]).
|
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-spec create_captcha(binary(), jid(), jid(),
|
2019-07-10 09:31:51 +02:00
|
|
|
binary(), any(),
|
|
|
|
callback() | term()) -> {error, image_error()} |
|
|
|
|
{ok, binary(), [text()], [xmpp_element()]}.
|
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} ->
|
2019-07-16 16:51:51 +02:00
|
|
|
Id = <<(p1_rand:get_string())/binary>>,
|
|
|
|
JID = jid:encode(From),
|
|
|
|
CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>,
|
|
|
|
Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image},
|
|
|
|
Fs = captcha_form:encode(
|
|
|
|
[{from, To}, {challenge, Id}, {sid, SID},
|
|
|
|
mk_ocr_field(Lang, CID, Type)],
|
|
|
|
Lang, [challenge]),
|
|
|
|
X = #xdata{type = form, fields = Fs},
|
|
|
|
Captcha = #xcaptcha{xdata = X},
|
2020-01-22 12:52:30 +01:00
|
|
|
BodyString = {?T("Your subscription request and/or messages to ~s have been blocked. "
|
|
|
|
"To unblock your subscription request, visit ~s"), [JID, get_url(Id)]},
|
2019-07-16 16:51:51 +02:00
|
|
|
Body = xmpp:mk_text(BodyString, Lang),
|
|
|
|
OOB = #oob_x{url = get_url(Id)},
|
|
|
|
Hint = #hint{type = 'no-store'},
|
|
|
|
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, [Hint, OOB, Captcha, Data]};
|
|
|
|
Err -> Err
|
2009-03-14 07:27:05 +01:00
|
|
|
end.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2016-07-28 14:10:41 +02:00
|
|
|
-spec create_captcha_x(binary(), jid(), binary(), any(), xdata()) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
{ok, [xmpp_element()]} | {error, image_error()}.
|
2016-07-28 14:10:41 +02:00
|
|
|
create_captcha_x(SID, To, Lang, Limiter, #xdata{fields = Fs} = X) ->
|
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} ->
|
2019-07-16 16:51:51 +02:00
|
|
|
Id = <<(p1_rand:get_string())/binary>>,
|
|
|
|
CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>,
|
|
|
|
Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image},
|
|
|
|
HelpTxt = translate:translate(
|
|
|
|
Lang, ?T("If you don't see the CAPTCHA image here, visit the web page.")),
|
|
|
|
Imageurl = get_url(<<Id/binary, "/image">>),
|
|
|
|
[H|T] = captcha_form:encode(
|
|
|
|
[{'captcha-fallback-text', HelpTxt},
|
|
|
|
{'captcha-fallback-url', Imageurl},
|
|
|
|
{from, To}, {challenge, Id}, {sid, SID},
|
|
|
|
mk_ocr_field(Lang, CID, Type)],
|
|
|
|
Lang, [challenge]),
|
|
|
|
Captcha = X#xdata{type = form, fields = [H|Fs ++ T]},
|
|
|
|
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(),
|
2019-06-14 11:33:26 +02:00
|
|
|
{xmlel(), cdata(),
|
2013-03-14 10:33:02 +01:00
|
|
|
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 = []},
|
2019-06-14 11:33:26 +02:00
|
|
|
Text = {xmlcdata, captcha_text(Lang)},
|
2013-03-14 10:33:02 +01:00
|
|
|
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 = []},
|
2019-06-14 11:33:26 +02:00
|
|
|
Text,
|
2013-03-14 10:33:02 +01:00
|
|
|
#xmlel{name = <<"br">>, attrs = [],
|
|
|
|
children = []},
|
|
|
|
IdEl, KeyEl,
|
|
|
|
#xmlel{name = <<"br">>, attrs = [],
|
|
|
|
children = []},
|
|
|
|
#xmlel{name = <<"input">>,
|
|
|
|
attrs =
|
|
|
|
[{<<"type">>, <<"submit">>},
|
|
|
|
{<<"name">>, <<"enter">>},
|
2019-06-22 16:08:45 +02:00
|
|
|
{<<"value">>, ?T("OK")}],
|
2013-03-14 10:33:02 +01:00
|
|
|
children = []}]},
|
2019-06-14 11:33:26 +02:00
|
|
|
{FormEl, {ImgEl, Text, IdEl, KeyEl}};
|
2013-03-14 10:33:02 +01:00
|
|
|
_ -> captcha_not_found
|
2009-03-30 13:55:31 +02:00
|
|
|
end.
|
|
|
|
|
2016-07-28 14:10:41 +02:00
|
|
|
-spec process_reply(xmpp_element()) -> ok | {error, bad_match | not_found | malformed}.
|
|
|
|
|
|
|
|
process_reply(#xdata{} = X) ->
|
2019-07-16 16:51:51 +02:00
|
|
|
Required = [<<"challenge">>, <<"ocr">>],
|
|
|
|
Fs = lists:filter(
|
|
|
|
fun(#xdata_field{var = Var}) ->
|
|
|
|
lists:member(Var, [<<"FORM_TYPE">>|Required])
|
|
|
|
end, X#xdata.fields),
|
|
|
|
try captcha_form:decode(Fs, [?NS_CAPTCHA], Required) of
|
|
|
|
Props ->
|
|
|
|
Id = proplists:get_value(challenge, Props),
|
|
|
|
OCR = proplists:get_value(ocr, Props),
|
2016-07-28 14:10:41 +02:00
|
|
|
case check_captcha(Id, OCR) of
|
|
|
|
captcha_valid -> ok;
|
|
|
|
captcha_non_valid -> {error, bad_match};
|
|
|
|
captcha_not_found -> {error, not_found}
|
2019-07-16 16:51:51 +02:00
|
|
|
end
|
|
|
|
catch _:{captcha_form, Why} ->
|
2019-09-23 14:17:20 +02:00
|
|
|
?WARNING_MSG("Malformed CAPTCHA form: ~ts",
|
2019-07-16 16:51:51 +02:00
|
|
|
[captcha_form:format_error(Why)]),
|
2016-07-28 14:10:41 +02:00
|
|
|
{error, malformed}
|
2009-03-14 07:27:05 +01:00
|
|
|
end;
|
2016-07-28 14:10:41 +02:00
|
|
|
process_reply(#xcaptcha{xdata = #xdata{} = X}) ->
|
|
|
|
process_reply(X);
|
|
|
|
process_reply(_) ->
|
|
|
|
{error, malformed}.
|
2009-03-30 13:55:31 +02:00
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec process_iq(iq()) -> iq().
|
2018-01-26 13:02:06 +01:00
|
|
|
process_iq(#iq{type = set, lang = Lang, sub_els = [#xcaptcha{} = El]} = IQ) ->
|
|
|
|
case process_reply(El) of
|
|
|
|
ok ->
|
|
|
|
xmpp:make_iq_result(IQ);
|
|
|
|
{error, malformed} ->
|
2019-06-22 16:08:45 +02:00
|
|
|
Txt = ?T("Incorrect CAPTCHA submit"),
|
2018-01-26 13:02:06 +01:00
|
|
|
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
|
|
|
|
{error, _} ->
|
2019-06-22 16:08:45 +02:00
|
|
|
Txt = ?T("The CAPTCHA verification has failed"),
|
2018-01-26 13:02:06 +01:00
|
|
|
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang))
|
|
|
|
end;
|
|
|
|
process_iq(#iq{type = get, lang = Lang} = IQ) ->
|
2019-06-22 16:08:45 +02:00
|
|
|
Txt = ?T("Value 'get' of 'type' attribute is not allowed"),
|
2018-01-26 13:02:06 +01:00
|
|
|
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
|
|
|
|
process_iq(#iq{lang = Lang} = IQ) ->
|
2019-06-22 16:08:45 +02:00
|
|
|
Txt = ?T("No module is handling this query"),
|
2018-01-26 13:02:06 +01:00
|
|
|
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
|
|
|
|
|
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
|
2019-06-14 11:33:26 +02:00
|
|
|
{FormEl, _} ->
|
2013-03-14 10:33:02 +01:00
|
|
|
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
|
2022-12-26 16:06:23 +01:00
|
|
|
{ok, Type, Key2, Img} ->
|
|
|
|
update_captcha_key(Id, Key, Key2),
|
2013-03-14 10:33:02 +01:00
|
|
|
{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,
|
2019-06-22 16:08:45 +02:00
|
|
|
?T("The CAPTCHA is valid."))}]},
|
2013-03-14 10:33:02 +01:00
|
|
|
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
|
|
|
|
2018-01-26 13:02:06 +01:00
|
|
|
host_up(Host) ->
|
|
|
|
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA,
|
2018-02-11 10:54:15 +01:00
|
|
|
?MODULE, process_iq).
|
2018-01-26 13:02:06 +01:00
|
|
|
|
|
|
|
host_down(Host) ->
|
|
|
|
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA).
|
|
|
|
|
|
|
|
config_reloaded() ->
|
|
|
|
gen_server:call(?MODULE, config_reloaded, timer:minutes(1)).
|
|
|
|
|
2009-03-13 17:02:59 +01:00
|
|
|
init([]) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
_ = mnesia:delete_table(captcha),
|
|
|
|
_ = ets:new(captcha, [named_table, public, {keypos, #captcha.id}]),
|
2018-01-26 13:02:06 +01:00
|
|
|
case check_captcha_setup() of
|
|
|
|
true ->
|
|
|
|
register_handlers(),
|
2022-12-28 17:11:11 +01:00
|
|
|
ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 70),
|
2018-01-26 13:02:06 +01:00
|
|
|
{ok, #state{enabled = true}};
|
|
|
|
false ->
|
|
|
|
{ok, #state{enabled = false}};
|
|
|
|
{error, Reason} ->
|
|
|
|
{stop, Reason}
|
|
|
|
end.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
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;
|
2018-01-26 13:02:06 +01:00
|
|
|
handle_call(config_reloaded, _From, #state{enabled = Enabled} = State) ->
|
|
|
|
State1 = case is_feature_available() of
|
|
|
|
true when not Enabled ->
|
|
|
|
case check_captcha_setup() of
|
|
|
|
true ->
|
|
|
|
register_handlers(),
|
|
|
|
State#state{enabled = true};
|
|
|
|
_ ->
|
|
|
|
State
|
|
|
|
end;
|
|
|
|
false when Enabled ->
|
|
|
|
unregister_handlers(),
|
|
|
|
State#state{enabled = false};
|
|
|
|
_ ->
|
|
|
|
State
|
|
|
|
end,
|
|
|
|
{reply, ok, State1};
|
2019-07-12 10:55:36 +02:00
|
|
|
handle_call(Request, From, State) ->
|
|
|
|
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
|
|
|
|
{noreply, State}.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2019-07-12 10:55:36 +02:00
|
|
|
handle_cast(Msg, State) ->
|
|
|
|
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
|
|
|
|
{noreply, State}.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
|
|
|
handle_info({remove_id, Id}, State) ->
|
2019-06-24 19:32:34 +02:00
|
|
|
?DEBUG("CAPTCHA ~p timed out", [Id]),
|
2013-03-14 10:33:02 +01:00
|
|
|
case ets:lookup(captcha, Id) of
|
2018-01-26 13:02:06 +01:00
|
|
|
[#captcha{args = Args, pid = Pid}] ->
|
|
|
|
callback(captcha_failed, Pid, Args),
|
|
|
|
ets:delete(captcha, Id);
|
|
|
|
_ -> ok
|
2013-03-14 10:33:02 +01:00
|
|
|
end,
|
2009-03-13 17:02:59 +01:00
|
|
|
{noreply, State};
|
2019-07-12 10:55:36 +02:00
|
|
|
handle_info(Info, State) ->
|
|
|
|
?WARNING_MSG("Unexpected info: ~p", [Info]),
|
|
|
|
{noreply, State}.
|
2009-03-13 17:02:59 +01:00
|
|
|
|
2018-01-26 13:02:06 +01:00
|
|
|
terminate(_Reason, #state{enabled = Enabled}) ->
|
|
|
|
if Enabled -> unregister_handlers();
|
|
|
|
true -> ok
|
|
|
|
end,
|
2023-01-03 19:13:43 +01:00
|
|
|
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 70).
|
2018-01-26 13:02:06 +01:00
|
|
|
|
|
|
|
register_handlers() ->
|
|
|
|
ejabberd_hooks:add(host_up, ?MODULE, host_up, 50),
|
|
|
|
ejabberd_hooks:add(host_down, ?MODULE, host_down, 50),
|
2019-06-14 11:33:26 +02:00
|
|
|
lists:foreach(fun host_up/1, ejabberd_option:hosts()).
|
2018-01-26 13:02:06 +01:00
|
|
|
|
|
|
|
unregister_handlers() ->
|
|
|
|
ejabberd_hooks:delete(host_up, ?MODULE, host_up, 50),
|
|
|
|
ejabberd_hooks:delete(host_down, ?MODULE, host_down, 50),
|
2019-06-14 11:33:26 +02:00
|
|
|
lists:foreach(fun host_down/1, ejabberd_option:hosts()).
|
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
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec create_image() -> {ok, binary(), binary(), binary()} |
|
|
|
|
{error, image_error()}.
|
|
|
|
create_image() ->
|
|
|
|
create_image(undefined).
|
2011-04-14 10:03:02 +02:00
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec create_image(term()) -> {ok, binary(), binary(), binary()} |
|
|
|
|
{error, image_error()}.
|
2011-04-14 10:03:02 +02:00
|
|
|
create_image(Limiter) ->
|
2018-07-05 10:51:49 +02:00
|
|
|
Key = str:substr(p1_rand:get_string(), 1, 6),
|
2011-04-14 10:03:02 +02:00
|
|
|
create_image(Limiter, Key).
|
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec create_image(term(), binary()) -> {ok, binary(), binary(), binary()} |
|
|
|
|
{error, image_error()}.
|
2011-04-14 10:03:02 +02:00
|
|
|
create_image(Limiter, Key) ->
|
|
|
|
case is_limited(Limiter) of
|
2019-06-14 11:33:26 +02: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
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec do_create_image(binary()) -> {ok, binary(), binary(), binary()} |
|
|
|
|
{error, image_error()}.
|
2011-04-14 10:03:02 +02:00
|
|
|
do_create_image(Key) ->
|
2009-03-13 17:02:59 +01:00
|
|
|
FileName = get_prog_name(),
|
2023-01-03 20:32:52 +01:00
|
|
|
case length(binary:split(FileName, <<"/">>)) == 1 of
|
2022-12-26 10:52:16 +01:00
|
|
|
true ->
|
|
|
|
do_create_image(Key, misc:binary_to_atom(FileName));
|
|
|
|
false ->
|
|
|
|
do_create_image(Key, FileName)
|
|
|
|
end.
|
|
|
|
|
|
|
|
do_create_image(Key, Module) when is_atom(Module) ->
|
|
|
|
Function = create_image,
|
|
|
|
erlang:apply(Module, Function, [Key]);
|
|
|
|
|
|
|
|
do_create_image(Key, FileName) when is_binary(FileName) ->
|
2019-09-23 14:17:20 +02:00
|
|
|
Cmd = lists:flatten(io_lib:format("~ts ~ts", [FileName, Key])),
|
2009-03-13 17:02:59 +01:00
|
|
|
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} ->
|
2019-09-23 14:17:20 +02:00
|
|
|
?ERROR_MSG("Failed to process output from \"~ts\". "
|
2013-03-14 10:33:02 +01:00
|
|
|
"Maybe ImageMagick's Convert program "
|
|
|
|
"is not installed.",
|
|
|
|
[Cmd]),
|
|
|
|
{error, Reason};
|
|
|
|
{error, Reason} ->
|
2019-09-23 14:17:20 +02:00
|
|
|
?ERROR_MSG("Failed to process an output from \"~ts\": ~p",
|
2013-03-14 10:33:02 +01:00
|
|
|
[Cmd, Reason]),
|
|
|
|
{error, Reason};
|
|
|
|
_ ->
|
|
|
|
Reason = malformed_image,
|
2019-09-23 14:17:20 +02:00
|
|
|
?ERROR_MSG("Failed to process an output from \"~ts\": ~p",
|
2013-03-14 10:33:02 +01:00
|
|
|
[Cmd, Reason]),
|
|
|
|
{error, Reason}
|
2009-03-13 17:02:59 +01:00
|
|
|
end.
|
|
|
|
|
|
|
|
get_prog_name() ->
|
2019-06-14 11:33:26 +02:00
|
|
|
case ejabberd_option:captcha_cmd() of
|
2013-03-14 10:33:02 +01:00
|
|
|
undefined ->
|
2023-02-01 11:52:47 +01:00
|
|
|
?WARNING_MSG("The option captcha_cmd is not configured, "
|
2013-03-14 10:33:02 +01:00
|
|
|
"but some module wants to use the CAPTCHA "
|
|
|
|
"feature.",
|
|
|
|
[]),
|
|
|
|
false;
|
|
|
|
FileName ->
|
2023-02-01 11:52:47 +01:00
|
|
|
maybe_warning_norequesthandler(),
|
2013-03-14 10:33:02 +01:00
|
|
|
FileName
|
2009-03-13 17:02:59 +01:00
|
|
|
end.
|
|
|
|
|
2023-02-01 11:52:47 +01:00
|
|
|
maybe_warning_norequesthandler() ->
|
|
|
|
Host = hd(ejabberd_option:hosts()),
|
|
|
|
URL = get_auto_url(any, ?MODULE, Host),
|
|
|
|
case URL of
|
|
|
|
undefined ->
|
|
|
|
?WARNING_MSG("The option captcha_cmd is configured, "
|
|
|
|
"but there is NO request_handler in listen option "
|
|
|
|
"configured with ejabberd_captcha. Please check "
|
|
|
|
"https://docs.ejabberd.im/admin/configuration/basic/#captcha",
|
|
|
|
[]);
|
|
|
|
_ ->
|
|
|
|
ok
|
|
|
|
end.
|
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec get_url(binary()) -> binary().
|
2009-03-13 17:02:59 +01:00
|
|
|
get_url(Str) ->
|
2019-06-14 15:06:04 +02:00
|
|
|
case ejabberd_option:captcha_url() of
|
2023-02-01 11:52:47 +01:00
|
|
|
auto ->
|
|
|
|
Host = ejabberd_config:get_myname(),
|
|
|
|
URL = get_auto_url(any, ?MODULE, Host),
|
|
|
|
<<URL/binary, $/, Str/binary>>;
|
2019-06-14 15:06:04 +02:00
|
|
|
undefined ->
|
|
|
|
URL = parse_captcha_host(),
|
|
|
|
<<URL/binary, "/captcha/", Str/binary>>;
|
|
|
|
URL ->
|
|
|
|
<<URL/binary, $/, Str/binary>>
|
|
|
|
end.
|
|
|
|
|
|
|
|
-spec parse_captcha_host() -> binary().
|
|
|
|
parse_captcha_host() ->
|
2019-06-14 11:33:26 +02:00
|
|
|
CaptchaHost = ejabberd_option:captcha_host(),
|
2013-03-14 10:33:02 +01:00
|
|
|
case str:tokens(CaptchaHost, <<":">>) of
|
2019-06-14 15:06:04 +02:00
|
|
|
[Host] ->
|
|
|
|
<<"http://", Host/binary>>;
|
|
|
|
[<<"http", _/binary>> = TransferProt, Host] ->
|
|
|
|
<<TransferProt/binary, ":", Host/binary>>;
|
|
|
|
[Host, PortString] ->
|
|
|
|
TransferProt = atom_to_binary(get_transfer_protocol(PortString), latin1),
|
|
|
|
<<TransferProt/binary, "://", Host/binary, ":", PortString/binary>>;
|
|
|
|
[TransferProt, Host, PortString] ->
|
|
|
|
<<TransferProt/binary, ":", Host/binary, ":", PortString/binary>>;
|
2013-03-14 10:33:02 +01:00
|
|
|
_ ->
|
2019-06-14 15:06:04 +02:00
|
|
|
<<"http://", (ejabberd_config:get_myname())/binary>>
|
2009-03-13 17:02:59 +01:00
|
|
|
end.
|
|
|
|
|
2023-02-01 11:52:47 +01:00
|
|
|
get_auto_url(Tls, Module, Host) ->
|
|
|
|
case find_handler_port_path(Tls, Module) of
|
|
|
|
[] -> undefined;
|
|
|
|
TPPs ->
|
|
|
|
{ThisTls, Port, Path} = case lists:keyfind(true, 1, TPPs) of
|
|
|
|
false ->
|
|
|
|
lists:keyfind(false, 1, TPPs);
|
|
|
|
TPP ->
|
|
|
|
TPP
|
|
|
|
end,
|
|
|
|
Protocol = case ThisTls of
|
|
|
|
false -> <<"http">>;
|
|
|
|
true -> <<"https">>
|
|
|
|
end,
|
|
|
|
<<Protocol/binary,
|
|
|
|
"://", Host/binary, ":",
|
|
|
|
(integer_to_binary(Port))/binary,
|
|
|
|
"/",
|
|
|
|
(str:join(Path, <<"/">>))/binary>>
|
|
|
|
end.
|
|
|
|
|
|
|
|
find_handler_port_path(Tls, Module) ->
|
|
|
|
lists:filtermap(
|
|
|
|
fun({{Port, _, _},
|
|
|
|
ejabberd_http,
|
|
|
|
#{tls := ThisTls, request_handlers := Handlers}})
|
|
|
|
when (Tls == any) or (Tls == ThisTls) ->
|
|
|
|
case lists:keyfind(Module, 2, Handlers) of
|
|
|
|
false -> false;
|
|
|
|
{Path, Module} -> {true, {ThisTls, Port, Path}}
|
|
|
|
end;
|
|
|
|
(_) -> false
|
|
|
|
end, ets:tab2list(ejabberd_listener)).
|
|
|
|
|
2011-03-03 11:35:47 +01:00
|
|
|
get_transfer_protocol(PortString) ->
|
2016-09-24 22:34:28 +02:00
|
|
|
PortNumber = 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) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
AllListeners = ejabberd_option:listen(),
|
2017-08-11 10:43:16 +02:00
|
|
|
lists:filter(
|
|
|
|
fun({{Port, _IP, _Transport}, _Module, _Opts}) ->
|
|
|
|
Port == PortNumber
|
|
|
|
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.">>);
|
2017-08-11 10:43:16 +02:00
|
|
|
get_captcha_transfer_protocol([{_, ejabberd_http, Opts} | Listeners]) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
Handlers = maps:get(request_handlers, Opts, []),
|
|
|
|
case lists:any(
|
|
|
|
fun({_, ?MODULE}) -> true;
|
|
|
|
({_, _}) -> false
|
|
|
|
end, Handlers) of
|
|
|
|
true ->
|
|
|
|
case maps:get(tls, Opts) of
|
2017-08-11 10:43:16 +02:00
|
|
|
true -> https;
|
|
|
|
false -> http
|
|
|
|
end;
|
2019-06-14 11:33:26 +02:00
|
|
|
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) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
case ejabberd_option:captcha_limit() of
|
|
|
|
infinity -> false;
|
2013-03-14 10:33:02 +01:00
|
|
|
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
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec cmd(string()) -> {ok, binary()} | {error, image_error()}.
|
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, <<>>).
|
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec recv_data(port(), reference(), binary()) -> {ok, binary()} | {error, image_error()}.
|
2009-03-13 17:02:59 +01:00
|
|
|
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.
|
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec return(port(), reference(), {ok, binary()} | {error, image_error()}) ->
|
|
|
|
{ok, binary()} | {error, image_error()}.
|
2009-03-13 17:02:59 +01:00
|
|
|
return(Port, TRef, Result) ->
|
2018-07-17 20:50:58 +02:00
|
|
|
misc:cancel_timer(TRef),
|
2009-03-13 17:02:59 +01:00
|
|
|
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;
|
2022-12-26 10:52:16 +01:00
|
|
|
MF when is_list(MF) -> true;
|
2013-03-14 10:33:02 +01:00
|
|
|
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
|
2018-01-26 13:02:06 +01:00
|
|
|
true ->
|
|
|
|
case create_image() of
|
|
|
|
{ok, _, _, _} ->
|
|
|
|
true;
|
|
|
|
Err ->
|
|
|
|
?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, "
|
|
|
|
"but it can't generate images.",
|
|
|
|
[]),
|
|
|
|
Err
|
|
|
|
end;
|
|
|
|
false ->
|
|
|
|
false
|
2013-03-14 10:33:02 +01:00
|
|
|
end.
|
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec lookup_captcha(binary()) -> {ok, #captcha{}} | {error, enoent}.
|
2013-03-14 10:33:02 +01:00
|
|
|
lookup_captcha(Id) ->
|
|
|
|
case ets:lookup(captcha, Id) of
|
|
|
|
[C] -> {ok, C};
|
2019-06-14 11:33:26 +02:00
|
|
|
[] -> {error, enoent}
|
2013-03-14 10:33:02 +01:00
|
|
|
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) ->
|
2019-06-14 11:33:26 +02:00
|
|
|
case lookup_captcha(Id) of
|
|
|
|
{ok, #captcha{pid = Pid, args = Args, key = ValidKey, tref = Tref}} ->
|
2018-01-26 13:02:06 +01:00
|
|
|
ets:delete(captcha, Id),
|
2018-07-17 20:50:58 +02:00
|
|
|
misc:cancel_timer(Tref),
|
2018-01-26 13:02:06 +01:00
|
|
|
if ValidKey == ProvidedKey ->
|
|
|
|
callback(captcha_succeed, Pid, Args),
|
|
|
|
captcha_valid;
|
|
|
|
true ->
|
|
|
|
callback(captcha_failed, Pid, Args),
|
|
|
|
captcha_non_valid
|
|
|
|
end;
|
2019-06-14 11:33:26 +02:00
|
|
|
{error, _} ->
|
2018-01-26 13:02:06 +01:00
|
|
|
captcha_not_found
|
2009-05-26 13:53:58 +02:00
|
|
|
end.
|
2011-04-14 10:03:02 +02:00
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec clean_treap(treap:treap(), priority()) -> treap:treap().
|
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.
|
|
|
|
|
2019-07-10 09:31:51 +02:00
|
|
|
-spec callback(captcha_succeed | captcha_failed,
|
|
|
|
pid() | undefined,
|
|
|
|
callback() | term()) -> any().
|
2018-01-26 13:02:06 +01:00
|
|
|
callback(Result, _Pid, F) when is_function(F) ->
|
|
|
|
F(Result);
|
|
|
|
callback(Result, Pid, Args) when is_pid(Pid) ->
|
|
|
|
Pid ! {Result, Args};
|
|
|
|
callback(_, _, _) ->
|
|
|
|
ok.
|
|
|
|
|
2019-06-14 11:33:26 +02:00
|
|
|
-spec now_priority() -> priority().
|
2011-04-14 10:03:02 +02:00
|
|
|
now_priority() ->
|
2019-02-27 09:56:20 +01:00
|
|
|
-erlang:system_time(microsecond).
|