ejabberd-contrib/mod_rest/src/mod_rest.erl

189 lines
6.8 KiB
Erlang

%%%-------------------------------------------------------------------
%%% File : mod_rest.erl
%%% Author : Nolan Eakins <sneakin@semanticgap.com>
%%% Purpose : Provide an HTTP interface to POST stanzas into ejabberd
%%%
%%% Copyright (C) 2008 Nolan Eakins
%%%
%%% 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.
%%%
%%% 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.
%%%
%%%-------------------------------------------------------------------
-module(mod_rest).
-author('sneakin@semanticgap.com').
-behavior(gen_mod).
-export([start/2,
stop/1,
depends/2,
split_line/1,
process/2,
mod_opt_type/1]).
-include("ejabberd.hrl").
-include("logger.hrl").
-include("ejabberd_http.hrl").
-include("ejabberd_ctl.hrl").
-include("xmpp.hrl").
start(_Host, _Opts) ->
?DEBUG("Starting: ~p ~p", [_Host, _Opts]),
ok.
stop(_Host) ->
ok.
depends(_Host, _Opts) ->
[].
process([], #request{method = 'POST', data = Data, host = Host, ip = ClientIp}) ->
try
{ClientAddress, _PortNumber} = ClientIp,
check_member_option(Host, ClientAddress, allowed_ips),
maybe_post_request(Data, Host, ClientIp)
catch
error:{badmatch, _} = Error ->
?DEBUG("Error when processing REST request: ~nData: ~p~nError: ~p", [Data, Error]),
{406, [], <<"Error: REST request is rejected by service.">>}
end;
process(Path, Request) ->
?DEBUG("Got request to ~p: ~p", [Path, Request]),
{200, [], <<"Try POSTing a stanza.">>}.
%% If the first character of Data is <, it is considered a stanza to deliver.
%% Otherwise, it is considered an ejabberd command to execute.
maybe_post_request(<<$<,_/binary>> = Data, Host, ClientIp) ->
try
Stanza = {xmlel, _, _, _} = fxml_stream:parse_element(Data),
Pkt = xmpp:decode(Stanza),
allowed = check_stanza(Pkt, Host),
?INFO_MSG("Got valid request with IP ~p:~n~p",
[ClientIp,
Pkt]),
post_request(Pkt)
catch
error:{badmatch, _} = Error ->
?DEBUG("Error when processing REST request: ~nData: ~p~nError: ~p", [Data, Error]),
{406, [], "Error: REST request is rejected by service."};
error:{Reason, _} = Error ->
?DEBUG("Error when processing REST request: ~nData: ~p~nError: ~p", [Data, Error]),
{500, [], "Error: " ++ atom_to_list(Reason)};
Error ->
?DEBUG("Error when processing REST request: ~nData: ~p~nError: ~p", [Data, Error]),
{500, [], "Error"}
end;
maybe_post_request(Data, Host, _ClientIp) ->
?INFO_MSG("Data: ~p", [Data]),
Args = split_line(unicode:characters_to_list(Data, utf8)),
Args2 = ensure_auth_is_provided(Args),
AccessCommands = get_option_access(Host),
case ejabberd_ctl:process2(Args2, AccessCommands) of
{"", ?STATUS_SUCCESS} ->
{200, [], integer_to_list(?STATUS_SUCCESS)};
{String, ?STATUS_SUCCESS} ->
{200, [], String};
{"", Code} ->
{200, [], integer_to_list(Code)};
{String, _Code} ->
{200, [], String}
end.
ensure_auth_is_provided(["--auth", _, _, _ | _] = Args) ->
Args;
ensure_auth_is_provided(Args) ->
["--auth", "", "", "" | Args].
%% This function throws an error if the module is not started in that VHost.
try_get_option(Host, OptionName, DefaultValue) ->
case gen_mod:is_loaded(Host, ?MODULE) of
true -> ok;
_ -> throw({module_must_be_started_in_vhost, ?MODULE, Host})
end,
gen_mod:get_module_opt(Host, ?MODULE, OptionName, fun(I) -> I end, DefaultValue).
get_option_access(Host) ->
try_get_option(Host, access_commands, []).
%% This function crashes if the stanza does not satisfy configured restrictions
check_stanza(Pkt, Host) ->
To = Pkt#message.to,
check_member_option(Host, jlib:jid_to_string(To), allowed_destinations),
%%+++ {xmlel, StanzaType, _Attrs, _Kids} = Stanza,
%%+++ check_member_option(Host, StanzaType, allowed_stanza_types),
allowed.
check_member_option(Host, ClientIp, allowed_ips) ->
true = case try_get_option(Host, allowed_ips, all) of
all -> true;
AllowedValues -> ip_matches(ClientIp, AllowedValues)
end;
check_member_option(Host, Element, Option) ->
true = case try_get_option(Host, Option, all) of
all -> true;
AllowedValues -> lists:member(Element, AllowedValues)
end.
ip_matches(ClientIp, AllowedValues) ->
lists:any(fun(El) ->
{ok, Net, Mask} = acl:parse_ip_netmask(El),
acl:acl_rule_matches({ip,{Net,Mask}}, #{ip => {ClientIp,port}}, host)
end,
AllowedValues).
post_request(Pkt) ->
mod_mam:user_send_packet({Pkt, #{jid => Pkt#message.from}}),
case ejabberd_router:route(Pkt) of
ok -> {200, [], <<"Ok">>};
_ -> {500, [], <<"Error">>}
end.
%% Split a line into args. Args are splitted by blankspaces. Args can be enclosed in "".
%%
%% Example call:
%% mod_rest:split_line(" a1 b2 \"c3 d4\"e5\" c6 d7 \\\" e8\"f9 g0 \\\" h1 ").
%% ["a1","b2","c3 d4\"e5","c6","d7"," e8\"f9 g0 ","h1"]
%%
%% 32 is the integer that represents the blankspace
%% 34 is the integer that represents the double quotes: "
%% 92 is the integer that represents the backslash: \
split_line(Line) -> split(Line, "", []).
split("", "", Args) -> lists:reverse(Args);
split("", Arg, Args) -> split("", "", [lists:reverse(Arg) | Args]);
split([32 | Line], "", Args) -> split(Line, [], Args);
split([32 | Line], Arg, Args) -> split(Line, [], [lists:reverse(Arg) | Args]);
split([34 | Line], "", Args) -> {Line2, Arg2} = splitend(Line), split([32 | Line2], Arg2, Args);
split([92, 34 | Line], "", Args) -> {Line2, Arg2} = splitend(Line), split([32 | Line2], Arg2, Args);
split([Char | Line], Arg, Args) -> split(Line, [Char | Arg], Args).
splitend(Line) -> splitend(Line, []).
splitend([], Res) -> {"", Res};
splitend([34], Res) -> {"", Res};
splitend([92, 34], Res) -> {"", Res};
splitend([34, 32 | Line], Res) -> {Line, Res};
splitend([92, 34, 32 | Line], Res) -> {Line, Res};
splitend([Char | Line], Res) -> splitend(Line, [Char | Res]).
mod_opt_type(allowed_ips) ->
fun (all) -> all; (A) when is_list(A) -> A end;
mod_opt_type(allowed_destinations) ->
fun (all) -> all; (A) when is_list(A) -> A end;
mod_opt_type(allowed_stanza_types) ->
fun (all) -> all; (A) when is_list(A) -> A end;
mod_opt_type(access_commands) ->
fun (A) when is_list(A) -> A end;
mod_opt_type(_) ->
[allowed_ips, allowed_destinations, allowed_stanza_types, access_commands].