Add OAuth support (thanks to Aleksey)

This commit is contained in:
Christophe Romain 2015-09-25 14:53:25 +02:00
parent 7cf904f4cf
commit a1129dc96b
13 changed files with 1207 additions and 67 deletions

View File

@ -108,10 +108,10 @@ AC_ARG_ENABLE(mssql,
esac],[db_type=generic])
AC_ARG_ENABLE(all,
[AC_HELP_STRING([--enable-all], [same as --enable-nif --enable-odbc --enable-mysql --enable-pgsql --enable-sqlite --enable-pam --enable-zlib --enable-riak --enable-redis --enable-elixir --enable-iconv --enable-debug --enable-lager --enable-tools (useful for Dialyzer checks, default: no)])],
[AC_HELP_STRING([--enable-all], [same as --enable-nif --enable-odbc --enable-mysql --enable-pgsql --enable-sqlite --enable-pam --enable-zlib --enable-riak --enable-redis --enable-elixir --enable-iconv --enable-debug --enable-lager --enable-tools --enable-oauth (useful for Dialyzer checks, default: no)])],
[case "${enableval}" in
yes) nif=true odbc=true mysql=true pgsql=true sqlite=true pam=true zlib=true riak=true redis=true elixir=true iconv=true debug=true lager=true tools=true ;;
no) nif=false odbc=false mysql=false pgsql=false sqlite=false pam=false zlib=false riak=false redis=false elixir=false iconv=false debug=false lager=false tools=false ;;
yes) nif=true odbc=true mysql=true pgsql=true sqlite=true pam=true zlib=true riak=true redis=true elixir=true iconv=true debug=true lager=true tools=true oauth=true ;;
no) nif=false odbc=false mysql=false pgsql=false sqlite=false pam=false zlib=false riak=false redis=false elixir=false iconv=false debug=false lager=false tools=false oauth=false ;;
*) AC_MSG_ERROR(bad value ${enableval} for --enable-all) ;;
esac],[])
@ -227,6 +227,14 @@ AC_ARG_ENABLE(lager,
*) AC_MSG_ERROR(bad value ${enableval} for --enable-lager) ;;
esac],[if test "x$lager" = "x"; then lager=true; fi])
AC_ARG_ENABLE(oauth,
[AC_HELP_STRING([--enable-oauth], [enable oauth support (default: yes)])],
[case "${enableval}" in
yes) oauth=true ;;
no) oauth=false ;;
*) AC_MSG_ERROR(bad value ${enableval} for --enable-oauth) ;;
esac],[if test "x$oauth" = "x"; then oauth=false; fi])
AC_CONFIG_FILES([Makefile
vars.config
src/ejabberd.app.src])
@ -273,5 +281,6 @@ AC_SUBST(iconv)
AC_SUBST(debug)
AC_SUBST(lager)
AC_SUBST(tools)
AC_SUBST(oauth)
AC_OUTPUT

View File

@ -34,6 +34,7 @@
module :: atom(),
function :: atom(),
args = [] :: [aterm()] | '_' | '$1' | '$2',
policy = restricted :: open | restricted | admin | user,
result = {res, rescode} :: rterm() | '_' | '$2'}).
-type ejabberd_commands() :: #ejabberd_commands{name :: atom(),

View File

@ -110,6 +110,11 @@ CfgDeps = lists:flatmap(
[{meck, "0.*", {git, "https://github.com/eproxus/meck"}}];
({redis, true}) ->
[{eredis, ".*", {git, "https://github.com/wooga/eredis"}}];
({oauth, true}) ->
%% ejabberd oauth support does not depends directly on xmlrpc.
%% However, we include xmlrpc as it is convenient to document and test oauth
[{oauth2, ".*", {git, "https://github.com/IvanMartinez/oauth2.git"}},
{xmlrpc, ".*", {git, "https://github.com/rds13/xmlrpc.git"}}];
(_) ->
[]
end, Cfg),

View File

@ -84,6 +84,7 @@ start() ->
cyrsasl_digest:start([]),
cyrsasl_scram:start([]),
cyrsasl_anonymous:start([]),
cyrsasl_oauth:start([]),
ok.
%%

96
src/cyrsasl_oauth.erl Normal file
View File

@ -0,0 +1,96 @@
%%%----------------------------------------------------------------------
%%% File : cyrsasl_oauth.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : X-OAUTH2 SASL mechanism
%%% Created : 17 Sep 2015 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2015 ProcessOne
%%%
%%% 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(cyrsasl_oauth).
-author('alexey@process-one.net').
-export([start/1, stop/0, mech_new/6, mech_step/2, parse/1]).
-behaviour(cyrsasl).
-record(state, {host, is_user_exists}).
start(_Opts) ->
cyrsasl:register_mechanism(<<"X-OAUTH2">>, ?MODULE, plain),
ok.
stop() -> ok.
mech_new(Host, _GetPassword, _CheckPassword, _CheckPasswordDigest,
IsUserExists, _ClientCertFile) ->
{ok, #state{host = Host, is_user_exists = IsUserExists}}.
mech_step(State, ClientIn) ->
case prepare(ClientIn) of
[AuthzId, User, Token] ->
case (State#state.is_user_exists)(User) of
true ->
case ejabberd_oauth:check_token(
User, State#state.host, <<"sasl_auth">>, Token) of
true ->
{ok,
[{username, User}, {authzid, AuthzId},
{auth_module, ejabberd_oauth}]};
false ->
{error, <<"not-authorized">>, User}
end;
_ -> {error, <<"not-authorized">>, User}
end;
_ -> {error, <<"bad-protocol">>}
end.
prepare(ClientIn) ->
case parse(ClientIn) of
[<<"">>, UserMaybeDomain, Token] ->
case parse_domain(UserMaybeDomain) of
%% <NUL>login@domain<NUL>pwd
[User, _Domain] -> [UserMaybeDomain, User, Token];
%% <NUL>login<NUL>pwd
[User] -> [<<"">>, User, Token]
end;
%% login@domain<NUL>login<NUL>pwd
[AuthzId, User, Token] -> [AuthzId, User, Token];
_ -> error
end.
parse(S) -> parse1(binary_to_list(S), "", []).
parse1([0 | Cs], S, T) ->
parse1(Cs, "", [list_to_binary(lists:reverse(S)) | T]);
parse1([C | Cs], S, T) -> parse1(Cs, [C | S], T);
%parse1([], [], T) ->
% lists:reverse(T);
parse1([], S, T) ->
lists:reverse([list_to_binary(lists:reverse(S)) | T]).
parse_domain(S) -> parse_domain1(binary_to_list(S), "", []).
parse_domain1([$@ | Cs], S, T) ->
parse_domain1(Cs, "", [list_to_binary(lists:reverse(S)) | T]);
parse_domain1([C | Cs], S, T) ->
parse_domain1(Cs, [C | S], T);
parse_domain1([], S, T) ->
lists:reverse([list_to_binary(lists:reverse(S)) | T]).

View File

@ -69,6 +69,7 @@ start(normal, _Args) ->
%ejabberd_debug:fprof_start(),
maybe_add_nameservers(),
ejabberd_auth:start(),
ejabberd_oauth:start(),
start_modules(),
ejabberd_listener:start_listeners(),
?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]),

View File

@ -211,12 +211,15 @@
-export([init/0,
list_commands/0,
get_command_format/1,
get_command_format/2,
get_command_definition/1,
get_tags_commands/0,
get_commands/0,
register_commands/1,
unregister_commands/1,
execute_command/2,
execute_command/4
execute_command/4,
opt_type/1
]).
-include("ejabberd_commands.hrl").
@ -265,19 +268,39 @@ list_commands() ->
_ = '_'}),
[{A, B, C} || [A, B, C] <- Commands].
-spec list_commands_policy() -> [{atom(), [aterm()], string(), atom()}].
%% @doc Get a list of all the available commands, arguments, description, and
%% policy.
list_commands_policy() ->
Commands = ets:match(ejabberd_commands,
#ejabberd_commands{name = '$1',
args = '$2',
desc = '$3',
policy = '$4',
_ = '_'}),
[{A, B, C, D} || [A, B, C, D] <- Commands].
-spec get_command_format(atom()) -> {[aterm()], rterm()} | {error, command_unknown}.
%% @doc Get the format of arguments and result of a command.
get_command_format(Name) ->
get_command_format(Name, noauth).
get_command_format(Name, Auth) ->
Admin = is_admin(Name, Auth),
Matched = ets:match(ejabberd_commands,
#ejabberd_commands{name = Name,
args = '$1',
result = '$2',
policy = '$3',
_ = '_'}),
case Matched of
[] ->
{error, command_unknown};
[[Args, Result]] ->
[[Args, Result, user]] when Admin ->
{[{user, binary}, {server, binary} | Args], Result};
[[Args, Result, _]] ->
{Args, Result}
end.
@ -295,24 +318,54 @@ get_command_definition(Name) ->
execute_command(Name, Arguments) ->
execute_command([], noauth, Name, Arguments).
-spec execute_command([{atom(), [atom()], [any()]}],
{binary(), binary(), binary(), boolean()} |
noauth | admin,
atom(),
[any()]
) -> any().
%% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> ResultTerm | {error, Error}
%% where
%% AccessCommands = [{Access, CommandNames, Arguments}]
%% Auth = {User::string(), Server::string(), Password::string()} | noauth
%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
%% | noauth
%% | admin
%% Method = atom()
%% Arguments = [any()]
%% Error = command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
execute_command(AccessCommands, Auth, Name, Arguments) ->
execute_command(AccessCommands1, Auth1, Name, Arguments) ->
Auth = case is_admin(Name, Auth1) of
true -> admin;
false -> Auth1
end,
case ets:lookup(ejabberd_commands, Name) of
[Command] ->
AccessCommands = get_access_commands(AccessCommands1),
try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of
ok -> execute_command2(Command, Arguments)
ok -> execute_command2(Auth, Command, Arguments)
catch
{error, Error} -> {error, Error}
end;
[] -> {error, command_unknown}
end.
execute_command2(
_Auth, #ejabberd_commands{policy = open} = Command, Arguments) ->
execute_command2(Command, Arguments);
execute_command2(
_Auth, #ejabberd_commands{policy = restricted} = Command, Arguments) ->
execute_command2(Command, Arguments);
execute_command2(
_Auth, #ejabberd_commands{policy = admin} = Command, Arguments) ->
execute_command2(Command, Arguments);
execute_command2(
admin, #ejabberd_commands{policy = user} = Command, Arguments) ->
execute_command2(Command, Arguments);
execute_command2(
{User, Server, _, _}, #ejabberd_commands{policy = user} = Command, Arguments) ->
execute_command2(Command, [User, Server | Arguments]).
execute_command2(Command, Arguments) ->
Module = Command#ejabberd_commands.module,
Function = Command#ejabberd_commands.function,
@ -372,11 +425,20 @@ get_tags_commands() ->
%% Error = account_unprivileged | invalid_account_data
check_access_commands([], _Auth, _Method, _Command, _Arguments) ->
ok;
check_access_commands(AccessCommands, Auth, Method, Command, Arguments) ->
check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) ->
Command =
case {Command1#ejabberd_commands.policy, Auth} of
{user, admin} ->
Command1#ejabberd_commands{
args = [{user, binary}, {server, binary} |
Command1#ejabberd_commands.args]};
_ ->
Command1
end,
AccessCommandsAllowed =
lists:filter(
fun({Access, Commands, ArgumentRestrictions}) ->
case check_access(Access, Auth) of
case check_access(Command, Access, Auth) of
true ->
check_access_command(Commands, Command, ArgumentRestrictions,
Method, Arguments);
@ -385,7 +447,7 @@ check_access_commands(AccessCommands, Auth, Method, Command, Arguments) ->
end;
({Access, Commands}) ->
ArgumentRestrictions = [],
case check_access(Access, Auth) of
case check_access(Command, Access, Auth) of
true ->
check_access_command(Commands, Command, ArgumentRestrictions,
Method, Arguments);
@ -399,29 +461,48 @@ check_access_commands(AccessCommands, Auth, Method, Command, Arguments) ->
L when is_list(L) -> ok
end.
-spec check_auth(noauth) -> noauth_provided;
({binary(), binary(), binary()}) -> {ok, binary(), binary()}.
-spec check_auth(ejabberd_commands(), noauth) -> noauth_provided;
(ejabberd_commands(),
{binary(), binary(), binary(), boolean()}) ->
{ok, binary(), binary()}.
check_auth(noauth) ->
check_auth(_Command, noauth) ->
no_auth_provided;
check_auth({User, Server, Password}) ->
check_auth(Command, {User, Server, {oauth, Token}, _}) ->
Scope = erlang:atom_to_binary(Command#ejabberd_commands.name, utf8),
case ejabberd_oauth:check_token(User, Server, Scope, Token) of
true ->
{ok, User, Server};
false ->
throw({error, invalid_account_data})
end;
check_auth(_Command, {User, Server, Password, _}) when is_binary(Password) ->
%% Check the account exists and password is valid
case ejabberd_auth:check_password(User, Server, Password) of
true -> {ok, User, Server};
_ -> throw({error, invalid_account_data})
end.
check_access(all, _) ->
check_access(Command, all, _)
when Command#ejabberd_commands.policy == open ->
true;
check_access(Access, Auth) ->
case check_auth(Auth) of
check_access(_Command, _Access, admin) ->
true;
check_access(_Command, _Access, {_User, _Server, _, true}) ->
false;
check_access(Command, Access, Auth)
when Command#ejabberd_commands.policy == open;
Command#ejabberd_commands.policy == user ->
case check_auth(Command, Auth) of
{ok, User, Server} ->
check_access(Access, User, Server);
check_access2(Access, User, Server);
_ ->
false
end.
end;
check_access(_Command, _Access, _Auth) ->
false.
check_access(Access, User, Server) ->
check_access2(Access, User, Server) ->
%% Check this user has access permission
case acl:match_rule(Server, Access, jlib:make_jid(User, Server, <<"">>)) of
allow -> true;
@ -452,3 +533,71 @@ tag_arguments(ArgsDefs, Args) ->
end,
ArgsDefs,
Args).
get_access_commands(undefined) ->
Cmds = get_commands(),
[{all, Cmds, []}];
get_access_commands(AccessCommands) ->
AccessCommands.
get_commands() ->
Opts = ejabberd_config:get_option(
commands,
fun(V) when is_list(V) -> V end,
[]),
CommandsList = list_commands_policy(),
OpenCmds = [N || {N, _, _, open} <- CommandsList],
RestrictedCmds = [N || {N, _, _, restricted} <- CommandsList],
AdminCmds = [N || {N, _, _, admin} <- CommandsList],
UserCmds = [N || {N, _, _, user} <- CommandsList],
Cmds =
lists:foldl(
fun({add_commands, L}, Acc) ->
Cmds = case L of
open -> OpenCmds;
restricted -> RestrictedCmds;
admin -> AdminCmds;
user -> UserCmds;
_ when is_list(L) -> L
end,
lists:usort(Cmds ++ Acc);
({remove_commands, L}, Acc) ->
Cmds = case L of
open -> OpenCmds;
restricted -> RestrictedCmds;
admin -> AdminCmds;
user -> UserCmds;
_ when is_list(L) -> L
end,
Acc -- Cmds;
(_, Acc) -> Acc
end, AdminCmds ++ UserCmds, Opts),
Cmds.
is_admin(_Name, noauth) ->
false;
is_admin(_Name, admin) ->
true;
is_admin(_Name, {_User, _Server, _, false}) ->
false;
is_admin(Name, {User, Server, _, true} = Auth) ->
AdminAccess = ejabberd_config:get_option(
commands_admin_access,
fun(A) when is_atom(A) -> A end,
none),
case acl:match_rule(Server, AdminAccess,
jlib:make_jid(User, Server, <<"">>)) of
allow ->
case catch check_auth(get_command_definition(Name), Auth) of
{ok, _, _} -> true;
_ -> false
end;
deny -> false
end.
opt_type(commands_admin_access) ->
fun(A) when is_atom(A) -> A end;
opt_type(commands) ->
fun(V) when is_list(V) -> V end;
opt_type(_) -> [commands, commands_admin_access].

View File

@ -211,7 +211,7 @@ process(Args) ->
process2(["--auth", User, Server, Pass | Args], AccessCommands) ->
process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass)}, AccessCommands);
process2(Args, AccessCommands) ->
process2(Args, noauth, AccessCommands).
process2(Args, admin, AccessCommands).
process2(Args, Auth, AccessCommands) ->
case try_run_ctp(Args, Auth, AccessCommands) of
@ -283,7 +283,7 @@ call_command([CmdString | Args], Auth, AccessCommands) ->
CmdStringU = ejabberd_regexp:greplace(
list_to_binary(CmdString), <<"-">>, <<"_">>),
Command = list_to_atom(binary_to_list(CmdStringU)),
case ejabberd_commands:get_command_format(Command) of
case ejabberd_commands:get_command_format(Command, Auth) of
{error, command_unknown} ->
{error, command_unknown};
{ArgsFormat, ResultFormat} ->
@ -412,7 +412,8 @@ get_list_commands() ->
end.
%% Return: {string(), [string()], string()}
tuple_command_help({Name, Args, Desc}) ->
tuple_command_help({Name, _Args, Desc}) ->
{Args, _} = ejabberd_commands:get_command_format(Name, admin),
Arguments = [atom_to_list(ArgN) || {ArgN, _ArgF} <- Args],
Prepend = case is_supported_args(Args) of
true -> "";
@ -723,12 +724,13 @@ print_usage_command(Cmd, C, MaxC, ShCode) ->
tags = TagsAtoms,
desc = Desc,
longdesc = LongDesc,
args = ArgsDef,
result = ResultDef} = C,
NameFmt = [" ", ?B("Command Name"), ": ", Cmd, "\n"],
%% Initial indentation of result is 13 = length(" Arguments: ")
{ArgsDef, _} = ejabberd_commands:get_command_format(
C#ejabberd_commands.name, admin),
Args = [format_usage_ctype(ArgDef, 13) || ArgDef <- ArgsDef],
ArgsMargin = lists:duplicate(13, $\s),
ArgsListFmt = case Args of

View File

@ -766,6 +766,9 @@ parse_auth(<<"Basic ", Auth64/binary>>) ->
{User, <<$:, Pass/binary>>} = erlang:split_binary(Auth, Pos-1),
{User, Pass}
end;
parse_auth(<<"Bearer ", SToken/binary>>) ->
Token = str:strip(SToken),
{oauth, Token, []};
parse_auth(<<_/binary>>) -> undefined.
parse_urlencoded(S) ->

473
src/ejabberd_oauth.erl Normal file

File diff suppressed because one or more lines are too long

View File

@ -198,33 +198,35 @@ socket_type() -> raw.
process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
AccessCommandsOpts = gen_mod:get_opt(access_commands, Opts,
fun(L) when is_list(L) -> L end,
[]),
AccessCommands = lists:flatmap(
fun({Ac, AcOpts}) ->
Commands = gen_mod:get_opt(
commands, AcOpts,
fun(A) when is_atom(A) ->
A;
(L) when is_list(L) ->
true = lists:all(
fun is_atom/1,
L),
L
end, all),
CommOpts = gen_mod:get_opt(
options, AcOpts,
fun(L) when is_list(L) -> L end,
[]),
[{Ac, Commands, CommOpts}];
(Wrong) ->
?WARNING_MSG("wrong options format for ~p: ~p",
[?MODULE, Wrong]),
[]
end, AccessCommandsOpts),
GetAuth = case [ACom || {Ac, _, _} = ACom <- AccessCommands, Ac /= all] of
[] -> false;
_ -> true
end,
undefined),
AccessCommands =
case AccessCommandsOpts of
undefined -> undefined;
_ ->
lists:flatmap(
fun({Ac, AcOpts}) ->
Commands = gen_mod:get_opt(
commands, AcOpts,
fun(A) when is_atom(A) ->
A;
(L) when is_list(L) ->
true = lists:all(
fun is_atom/1,
L),
L
end, all),
CommOpts = gen_mod:get_opt(
options, AcOpts,
fun(L) when is_list(L) -> L end,
[]),
[{Ac, Commands, CommOpts}];
(Wrong) ->
?WARNING_MSG("wrong options format for ~p: ~p",
[?MODULE, Wrong]),
[]
end, AccessCommandsOpts)
end,
GetAuth = true,
State = #state{access_commands = AccessCommands, get_auth = GetAuth},
case xml_stream:parse_element(Data) of
{error, _} ->
@ -257,17 +259,22 @@ process(_, _) ->
%% -----------------------------
get_auth(AuthList) ->
[User, Server, Password] = try get_attrs([user, server,
password],
AuthList)
of
[U, S, P] -> [U, S, P]
catch
exit:{attribute_not_found, Attr, _} ->
throw({error, missing_auth_arguments,
Attr})
end,
{User, Server, Password}.
Admin =
case lists:keysearch(admin, 1, AuthList) of
{value, {admin, true}} -> true;
_ -> false
end,
try get_attrs([user, server, token], AuthList) of
[U, S, T] -> {U, S, {oauth, T}, Admin}
catch
exit:{attribute_not_found, _Attr, _} ->
try get_attrs([user, server, password], AuthList) of
[U, S, P] -> {U, S, P, Admin}
catch
exit:{attribute_not_found, Attr, _} ->
throw({error, missing_auth_arguments, Attr})
end
end.
%% -----------------------------
%% Handlers
@ -297,9 +304,9 @@ handler(#state{get_auth = true, auth = noauth} = State,
{call, Method,
[{struct, AuthList} | Arguments] = AllArgs}) ->
try get_auth(AuthList) of
Auth ->
handler(State#state{get_auth = false, auth = Auth},
{call, Method, Arguments})
Auth ->
handler(State#state{get_auth = false, auth = Auth},
{call, Method, Arguments})
catch
{error, missing_auth_arguments, _Attr} ->
handler(State#state{get_auth = false, auth = noauth},
@ -328,7 +335,7 @@ handler(State, {call, Command, []}) ->
handler(State, {call, Command, [{struct, []}]});
handler(State,
{call, Command, [{struct, AttrL}]} = Payload) ->
case ejabberd_commands:get_command_format(Command) of
case ejabberd_commands:get_command_format(Command, State#state.auth) of
{error, command_unknown} ->
build_fault_response(-112, "Unknown call: ~p",
[Payload]);

392
src/mod_http_api.erl Normal file
View File

@ -0,0 +1,392 @@
%%%----------------------------------------------------------------------
%%% File : mod_http_api.erl
%%% Author : Christophe romain <christophe.romain@process-one.net>
%%% Purpose : Implements REST API for ejabberd using JSON data
%%% Created : 15 Sep 2014 by Christophe Romain <christophe.romain@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2015 ProcessOne
%%%
%%% 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.
%%%
%%%----------------------------------------------------------------------
%% Example config:
%%
%% in ejabberd_http listener
%% request_handlers:
%% "/api": mod_http_api
%%
%% Access rights are defined with:
%% commands_admin_access: configure
%% commands:
%% - add_commands: user
%%
%%
%% add_commands allow exporting a class of commands, from
%% open: methods is not risky and can be called by without any access check
%% restricted (default): the same, but will appear only in ejabberdctl list.
%% admin auth is required with XMLRPC and HTTP API and checked for admin priviledges, works as usual in ejabberdctl.
%% user - can be used through XMLRPC and HTTP API, even by user. Only admin can use the commands for other users.
%%
%% Then to perform an action, send a POST request to the following URL:
%% http://localhost:5280/api/<call_name>
-module(mod_http_api).
-author('cromain@process-one.net').
-behaviour(gen_mod).
-export([start/2, stop/1, process/2, mod_opt_type/1]).
-include("ejabberd.hrl").
-include("jlib.hrl").
-include("logger.hrl").
-include("ejabberd_http.hrl").
-define(CT_PLAIN,
{<<"Content-Type">>, <<"text/plain">>}).
-define(CT_XML,
{<<"Content-Type">>, <<"text/xml; charset=utf-8">>}).
-define(CT_JSON,
{<<"Content-Type">>, <<"application/json">>}).
-define(AC_ALLOW_ORIGIN,
{<<"Access-Control-Allow-Origin">>, <<"*">>}).
-define(AC_ALLOW_METHODS,
{<<"Access-Control-Allow-Methods">>,
<<"GET, POST, OPTIONS">>}).
-define(AC_ALLOW_HEADERS,
{<<"Access-Control-Allow-Headers">>,
<<"Content-Type">>}).
-define(AC_MAX_AGE,
{<<"Access-Control-Max-Age">>, <<"86400">>}).
-define(OPTIONS_HEADER,
[?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS,
?AC_ALLOW_HEADERS, ?AC_MAX_AGE]).
-define(HEADER(CType),
[CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]).
%% -------------------
%% Module control
%% -------------------
start(_Host, _Opts) ->
ok.
stop(_Host) ->
ok.
%% ----------
%% basic auth
%% ----------
check_permissions(#request{auth = HTTPAuth, headers = Headers}, Command)
when HTTPAuth /= undefined ->
case catch binary_to_existing_atom(Command, utf8) of
Call when is_atom(Call) ->
Admin =
case lists:keysearch(<<"X-Admin">>, 1, Headers) of
{value, {_, <<"true">>}} -> true;
_ -> false
end,
Auth =
case HTTPAuth of
{SJID, Pass} ->
case jlib:string_to_jid(SJID) of
#jid{user = User, server = Server} ->
case ejabberd_auth:check_password(User, Server, Pass) of
true -> {ok, {User, Server, Pass, Admin}};
false -> false
end;
_ ->
false
end;
{oauth, Token, _} ->
case ejabberd_oauth:check_token(Command, Token) of
{ok, User, Server} ->
{ok, {User, Server, {oauth, Token}, Admin}};
false ->
false
end;
_ ->
false
end,
case Auth of
{ok, A} -> {allowed, Call, A};
_ -> unauthorized_response()
end;
_ ->
unauthorized_response()
end;
check_permissions(_, _Command) ->
unauthorized_response().
%% ------------------
%% command processing
%% ------------------
process(_, #request{method = 'POST', data = <<>>}) ->
?DEBUG("Bad Request: no data", []),
badrequest_response();
process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) ->
try
Args = case jiffy:decode(Data) of
List when is_list(List) -> List;
{List} when is_list(List) -> List;
Other -> [Other]
end,
log(Call, Args, IP),
case check_permissions(Req, Call) of
{allowed, Cmd, Auth} ->
{Code, Result} = handle(Cmd, Auth, Args),
json_response(Code, jiffy:encode(Result));
ErrorResponse ->
ErrorResponse
end
catch _:Error ->
?DEBUG("Bad Request: ~p", [Error]),
badrequest_response()
end;
process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
try
Args = case Data of
[{nokey, <<>>}] -> [];
_ -> Data
end,
log(Call, Args, IP),
case check_permissions(Req, Call) of
{allowed, Cmd, Auth} ->
{Code, Result} = handle(Cmd, Auth, Args),
json_response(Code, jiffy:encode(Result));
ErrorResponse ->
ErrorResponse
end
catch _:Error ->
?DEBUG("Bad Request: ~p", [Error]),
badrequest_response()
end;
process([], #request{method = 'OPTIONS', data = <<>>}) ->
{200, ?OPTIONS_HEADER, []};
process(_Path, Request) ->
?DEBUG("Bad Request: no handler ~p", [Request]),
badrequest_response().
%% ----------------
%% command handlers
%% ----------------
% generic ejabberd command handler
handle(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
case ejabberd_commands:get_command_format(Call, Auth) of
{ArgsSpec, _} when is_list(ArgsSpec) ->
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
Spec = lists:foldr(
fun ({Key, binary}, Acc) ->
[{Key, <<>>}|Acc];
({Key, string}, Acc) ->
[{Key, <<>>}|Acc];
({Key, integer}, Acc) ->
[{Key, 0}|Acc];
({Key, {list, _}}, Acc) ->
[{Key, []}|Acc];
({Key, atom}, Acc) ->
[{Key, undefined}|Acc]
end, [], ArgsSpec),
handle2(Call, Auth, match(Args2, Spec));
{error, Msg} ->
{400, Msg};
_Error ->
{400, <<"Error">>}
end.
handle2(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
{ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth),
ArgsFormatted = format_args(Args, ArgsF),
case ejabberd_command(Auth, Call, ArgsFormatted, 400) of
0 -> {200, <<"OK">>};
1 -> {500, <<"500 Internal server error">>};
400 -> {400, <<"400 Bad Request">>};
404 -> {404, <<"404 Not found">>};
Res -> format_command_result(Call, Auth, Res)
end.
get_elem_delete(A, L) ->
case proplists:get_all_values(A, L) of
[Value] -> {Value, proplists:delete(A, L)};
[_, _ | _] ->
%% Crash reporting the error
exit({duplicated_attribute, A, L});
[] ->
%% Report the error and then force a crash
exit({attribute_not_found, A, L})
end.
format_args(Args, ArgsFormat) ->
{ArgsRemaining, R} = lists:foldl(fun ({ArgName,
ArgFormat},
{Args1, Res}) ->
{ArgValue, Args2} =
get_elem_delete(ArgName,
Args1),
Formatted = format_arg(ArgValue,
ArgFormat),
{Args2, Res ++ [Formatted]}
end,
{Args, []}, ArgsFormat),
case ArgsRemaining of
[] -> R;
L when is_list(L) -> exit({additional_unused_args, L})
end.
format_arg({array, Elements},
{list, {ElementDefName, ElementDefFormat}})
when is_list(Elements) ->
lists:map(fun ({struct, [{ElementName, ElementValue}]}) when
ElementDefName == ElementName ->
format_arg(ElementValue, ElementDefFormat)
end,
Elements);
format_arg({array, [{struct, Elements}]},
{list, {ElementDefName, ElementDefFormat}})
when is_list(Elements) ->
lists:map(fun ({ElementName, ElementValue}) ->
true = ElementDefName == ElementName,
format_arg(ElementValue, ElementDefFormat)
end,
Elements);
format_arg({array, [{struct, Elements}]},
{tuple, ElementsDef})
when is_list(Elements) ->
FormattedList = format_args(Elements, ElementsDef),
list_to_tuple(FormattedList);
format_arg({array, Elements}, {list, ElementsDef})
when is_list(Elements) and is_atom(ElementsDef) ->
[format_arg(Element, ElementsDef)
|| Element <- Elements];
format_arg(Arg, integer) when is_integer(Arg) -> Arg;
format_arg(Arg, binary) when is_list(Arg) -> process_unicode_codepoints(Arg);
format_arg(Arg, binary) when is_binary(Arg) -> Arg;
format_arg(Arg, string) when is_list(Arg) -> process_unicode_codepoints(Arg);
format_arg(Arg, string) when is_binary(Arg) -> Arg;
format_arg(undefined, binary) -> <<>>;
format_arg(undefined, string) -> <<>>;
format_arg(Arg, Format) ->
?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]),
error.
process_unicode_codepoints(Str) ->
iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]);
(Y) -> Y
end, Str)).
%% ----------------
%% internal helpers
%% ----------------
match(Args, Spec) ->
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
ejabberd_command(Auth, Cmd, Args, Default) ->
case catch ejabberd_commands:execute_command(undefined, Auth, Cmd, Args) of
{'EXIT', _} -> Default;
{error, _} -> Default;
Result -> Result
end.
format_command_result(Cmd, Auth, Result) ->
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth),
case {ResultFormat, Result} of
{{_, rescode}, V} when V == true; V == ok ->
{200, <<"">>};
{{_, rescode}, _} ->
{500, <<"">>};
{{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
{200, iolist_to_binary(Text1)};
{{_, restuple}, {_, Text2}} ->
{500, iolist_to_binary(Text2)};
{{_, {list, _}}, _V} ->
{_, L} = format_result(Result, ResultFormat),
{200, L};
{{_, {tuple, _}}, _V} ->
{_, T} = format_result(Result, ResultFormat),
{200, T};
_ ->
{200, {[format_result(Result, ResultFormat)]}}
end.
format_result(Atom, {Name, atom}) ->
{jlib:atom_to_binary(Name), jlib:atom_to_binary(Atom)};
format_result(Int, {Name, integer}) ->
{jlib:atom_to_binary(Name), Int};
format_result(String, {Name, string}) ->
{jlib:atom_to_binary(Name), iolist_to_binary(String)};
format_result(Code, {Name, rescode}) ->
{jlib:atom_to_binary(Name), Code == true orelse Code == ok};
format_result({Code, Text}, {Name, restuple}) ->
{jlib:atom_to_binary(Name),
{[{<<"res">>, Code == true orelse Code == ok},
{<<"text">>, iolist_to_binary(Text)}]}};
format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) ->
{jlib:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
format_result(Els, {Name, {list, Def}}) ->
{jlib:atom_to_binary(Name), [element(2, format_result(El, Def)) || El <- Els]};
format_result(Tuple, {_Name, {tuple, [{_, atom}, ValFmt]}}) ->
{Name2, Val} = Tuple,
{_, Val2} = format_result(Val, ValFmt),
{jlib:atom_to_binary(Name2), Val2};
format_result(Tuple, {Name, {tuple, Def}}) ->
Els = lists:zip(tuple_to_list(Tuple), Def),
{jlib:atom_to_binary(Name), {[format_result(El, ElDef) || {El, ElDef} <- Els]}};
format_result(404, {_Name, _}) ->
"not_found".
unauthorized_response() ->
{401, ?HEADER(?CT_XML),
#xmlel{name = <<"h1">>, attrs = [],
children = [{xmlcdata, <<"401 Unauthorized">>}]}}.
badrequest_response() ->
{400, ?HEADER(?CT_XML),
#xmlel{name = <<"h1">>, attrs = [],
children = [{xmlcdata, <<"400 Bad Request">>}]}}.
json_response(Code, Body) when is_integer(Code) ->
{Code, ?HEADER(?CT_JSON), Body}.
log(Call, Args, {Addr, Port}) ->
AddrS = jlib:ip_to_list({Addr, Port}),
?INFO_MSG("Admin call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]).
mod_opt_type(access) ->
fun(Access) when is_atom(Access) -> Access end;
mod_opt_type(_) -> [access].

View File

@ -31,6 +31,7 @@
{elixir, @elixir@}.
{lager, @lager@}.
{iconv, @iconv@}.
{oauth, @oauth@}.
%% Version
{vsn, "@PACKAGE_VERSION@"}.