diff options
author | Christophe Romain <christophe.romain@process-one.net> | 2015-09-25 14:53:25 +0200 |
---|---|---|
committer | Christophe Romain <christophe.romain@process-one.net> | 2015-09-25 15:49:07 +0200 |
commit | a1129dc96b4782800c3046ea6de6a77049f2293e (patch) | |
tree | 16d24c6eefbe7a5f65c0ac4410d8a57eed1d1222 | |
parent | Merge pull request #742 from joudinet/master (diff) |
Add OAuth support (thanks to Aleksey)
Diffstat (limited to '')
-rw-r--r-- | configure.ac | 15 | ||||
-rw-r--r-- | include/ejabberd_commands.hrl | 1 | ||||
-rw-r--r-- | rebar.config.script | 5 | ||||
-rw-r--r-- | src/cyrsasl.erl | 1 | ||||
-rw-r--r-- | src/cyrsasl_oauth.erl | 96 | ||||
-rw-r--r-- | src/ejabberd_app.erl | 1 | ||||
-rw-r--r-- | src/ejabberd_commands.erl | 185 | ||||
-rw-r--r-- | src/ejabberd_ctl.erl | 10 | ||||
-rw-r--r-- | src/ejabberd_http.erl | 3 | ||||
-rw-r--r-- | src/ejabberd_oauth.erl | 473 | ||||
-rw-r--r-- | src/ejabberd_xmlrpc.erl | 91 | ||||
-rw-r--r-- | src/mod_http_api.erl | 392 | ||||
-rw-r--r-- | vars.config.in | 1 |
13 files changed, 1207 insertions, 67 deletions
diff --git a/configure.ac b/configure.ac index d30a490d..69fd3cef 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index 3ab15ca3..82ee891c 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -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(), diff --git a/rebar.config.script b/rebar.config.script index 7ec52722..2ecb39c2 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -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), diff --git a/src/cyrsasl.erl b/src/cyrsasl.erl index 970da5bb..518bb84f 100644 --- a/src/cyrsasl.erl +++ b/src/cyrsasl.erl @@ -84,6 +84,7 @@ start() -> cyrsasl_digest:start([]), cyrsasl_scram:start([]), cyrsasl_anonymous:start([]), + cyrsasl_oauth:start([]), ok. %% diff --git a/src/cyrsasl_oauth.erl b/src/cyrsasl_oauth.erl new file mode 100644 index 00000000..fc5a9c6c --- /dev/null +++ b/src/cyrsasl_oauth.erl @@ -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]). diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 319e494c..657785ff 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -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()]), diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index a5ba98ae..34297e00 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -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(_Command, _Access, admin) -> true; -check_access(Access, Auth) -> - case check_auth(Auth) of +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]. diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 82ac6b13..8b6f1852 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -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 diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index 61ce6473..4c647ab5 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -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) -> diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl new file mode 100644 index 00000000..0295b9d8 --- /dev/null +++ b/src/ejabberd_oauth.erl @@ -0,0 +1,473 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_oauth.erl +%%% Author : Alexey Shchepin <alexey@process-one.net> +%%% Purpose : OAUTH2 support +%%% Created : 20 Mar 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +-module(ejabberd_oauth). + +-behaviour(gen_server). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3]). + +-export([start/0, + start_link/0, + get_client_identity/2, + verify_redirection_uri/3, + authenticate_user/2, + authenticate_client/2, + verify_resowner_scope/3, + associate_access_code/3, + associate_access_token/3, + associate_refresh_token/3, + check_token/4, + check_token/2, + process/2, + opt_type/1]). + +-include("jlib.hrl"). + +-include("ejabberd.hrl"). +-include("logger.hrl"). + +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). + +-record(oauth_token, { + token = {<<"">>, <<"">>} :: {binary(), binary()}, + us = {<<"">>, <<"">>} :: {binary(), binary()}, + scope = [] :: [binary()], + expire :: integer() + }). + +-define(EXPIRE, 3600). + +start() -> + init_db(mnesia, ?MYNAME), + Expire = expire(), + application:set_env(oauth2, backend, ejabberd_oauth), + application:set_env(oauth2, expiry_time, Expire), + application:start(oauth2), + ChildSpec = {?MODULE, {?MODULE, start_link, []}, + temporary, 1000, worker, [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec), + ok. + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + + +init([]) -> + erlang:send_after(expire() * 1000, self(), clean), + {ok, ok}. + +handle_call(_Request, _From, State) -> + {reply, bad_request, State}. + +handle_cast(_Msg, State) -> {noreply, State}. + +handle_info(clean, State) -> + {MegaSecs, Secs, MiniSecs} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + F = fun() -> + Ts = mnesia:select( + oauth_token, + [{#oauth_token{expire = '$1', _ = '_'}, + [{'<', '$1', TS}], + ['$_']}]), + lists:foreach(fun mnesia:delete_object/1, Ts) + end, + mnesia:async_dirty(F), + erlang:send_after(trunc(expire() * 1000 * (1 + MiniSecs / 1000000)), + self(), clean), + {noreply, State}; +handle_info(_Info, State) -> {noreply, State}. + +terminate(_Reason, _State) -> ok. + +code_change(_OldVsn, State, _Extra) -> {ok, State}. + + +init_db(mnesia, _Host) -> + mnesia:create_table(oauth_token, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, oauth_token)}]), + mnesia:add_table_copy(oauth_token, node(), disc_copies); +init_db(_, _) -> + ok. + + +get_client_identity(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. + +verify_redirection_uri(_, _, Ctx) -> {ok, Ctx}. + +authenticate_user({User, Server}, {password, Password} = Ctx) -> + case ejabberd_auth:check_password(User, Server, Password) of + true -> + {ok, {Ctx, {user, User, Server}}}; + false -> + {error, badpass} + end. + +authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. + +verify_resowner_scope({user, User, Server}, Scope, Ctx) -> + Cmds = ejabberd_commands:get_commands(), + Cmds1 = [sasl_auth | Cmds], + RegisteredScope = [atom_to_binary(C, utf8) || C <- Cmds1], + case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), + oauth2_priv_set:new(RegisteredScope)) of + true -> + {ok, {Ctx, Scope}}; + false -> + {error, badscope} + end; +verify_resowner_scope(_, _, _) -> + {error, badscope}. + + +associate_access_code(AccessCode, Context, AppContext) -> + %put(?ACCESS_CODE_TABLE, AccessCode, Context), + {ok, AppContext}. + +associate_access_token(AccessToken, Context, AppContext) -> + {user, User, Server} = + proplists:get_value(<<"resource_owner">>, Context, <<"">>), + Scope = proplists:get_value(<<"scope">>, Context, []), + Expire = proplists:get_value(<<"expiry_time">>, Context, 0), + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + R = #oauth_token{ + token = AccessToken, + us = {LUser, LServer}, + scope = Scope, + expire = Expire + }, + mnesia:dirty_write(R), + {ok, AppContext}. + +associate_refresh_token(RefreshToken, Context, AppContext) -> + %put(?REFRESH_TOKEN_TABLE, RefreshToken, Context), + {ok, AppContext}. + + +check_token(User, Server, Scope, Token) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + case catch mnesia:dirty_read(oauth_token, Token) of + [#oauth_token{us = {LUser, LServer}, + scope = TokenScope, + expire = Expire}] -> + {MegaSecs, Secs, _} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + oauth2_priv_set:is_member( + Scope, oauth2_priv_set:new(TokenScope)) andalso + Expire > TS; + _ -> + false + end. + +check_token(Scope, Token) -> + case catch mnesia:dirty_read(oauth_token, Token) of + [#oauth_token{us = {LUser, LServer}, + scope = TokenScope, + expire = Expire}] -> + {MegaSecs, Secs, _} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + case oauth2_priv_set:is_member( + Scope, oauth2_priv_set:new(TokenScope)) andalso + Expire > TS of + true -> {ok, LUser, LServer}; + false -> false + end; + _ -> + false + end. + + +expire() -> + ejabberd_config:get_option( + oauth_expire, + fun(I) when is_integer(I) -> I end, + ?EXPIRE). + +-define(DIV(Class, Els), + ?XAE(<<"div">>, [{<<"class">>, Class}], Els)). +-define(INPUTID(Type, Name, Value), + ?XA(<<"input">>, + [{<<"type">>, Type}, {<<"name">>, Name}, + {<<"value">>, Value}, {<<"id">>, Name}])). +-define(LABEL(ID, Els), + ?XAE(<<"label">>, [{<<"for">>, ID}], Els)). + +process(_Handlers, + #request{method = 'GET', q = Q, lang = Lang, + path = [_, <<"authorization_token">>]}) -> + ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>), + ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>), + RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>), + Scope = proplists:get_value(<<"scope">>, Q, <<"">>), + State = proplists:get_value(<<"state">>, Q, <<"">>), + Form = + ?XAE(<<"form">>, + [{<<"action">>, <<"authorization_token">>}, + {<<"method">>, <<"post">>}], + [?LABEL(<<"username">>, [?CT(<<"User">>), ?C(<<": ">>)]), + ?INPUTID(<<"text">>, <<"username">>, <<"">>), + ?BR, + ?LABEL(<<"server">>, [?CT(<<"Server">>), ?C(<<": ">>)]), + ?INPUTID(<<"text">>, <<"server">>, <<"">>), + ?BR, + ?LABEL(<<"password">>, [?CT(<<"Password">>), ?C(<<": ">>)]), + ?INPUTID(<<"password">>, <<"password">>, <<"">>), + ?INPUT(<<"hidden">>, <<"response_type">>, ResponseType), + ?INPUT(<<"hidden">>, <<"client_id">>, ClientId), + ?INPUT(<<"hidden">>, <<"redirect_uri">>, RedirectURI), + ?INPUT(<<"hidden">>, <<"scope">>, Scope), + ?INPUT(<<"hidden">>, <<"state">>, State), + ?BR, + ?INPUTT(<<"submit">>, <<"">>, <<"Accept">>) + ]), + Top = + ?DIV(<<"section">>, + [?DIV(<<"block">>, + [?A(<<"https://www.ejabberd.im">>, + [?XA(<<"img">>, + [{<<"height">>, <<"32">>}, + {<<"src">>, logo()}])] + )])]), + Middle = + ?DIV(<<"white section">>, + [?DIV(<<"block">>, + [?XC(<<"h1">>, <<"Authorization request">>), + ?XE(<<"p">>, + [?C(<<"Application ">>), + ?XC(<<"em">>, ClientId), + ?C(<<" wants to access scope ">>), + ?XC(<<"em">>, Scope)]), + Form + ])]), + Bottom = + ?DIV(<<"section">>, + [?DIV(<<"block">>, + [?XAC(<<"a">>, + [{<<"href">>, <<"https://www.ejabberd.im">>}, + {<<"title">>, <<"ejabberd XMPP server">>}], + <<"ejabberd">>), + ?C(" is maintained by "), + ?XAC(<<"a">>, + [{<<"href">>, <<"https://www.process-one.net">>}, + {<<"title">>, <<"ProcessOne - Leader in Instant Messaging and Push Solutions">>}], + <<"ProcessOne">>) + ])]), + Body = ?DIV(<<"container">>, [Top, Middle, Bottom]), + ejabberd_web:make_xhtml(web_head(), [Body]); +process(_Handlers, + #request{method = 'POST', q = Q, lang = _Lang, + path = [_, <<"authorization_token">>]}) -> + ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>), + ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>), + RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>), + SScope = proplists:get_value(<<"scope">>, Q, <<"">>), + Username = proplists:get_value(<<"username">>, Q, <<"">>), + Server = proplists:get_value(<<"server">>, Q, <<"">>), + Password = proplists:get_value(<<"password">>, Q, <<"">>), + State = proplists:get_value(<<"state">>, Q, <<"">>), + Scope = str:tokens(SScope, <<" ">>), + case oauth2:authorize_password({Username, Server}, + ClientId, + RedirectURI, + Scope, + {password, Password}) of + {ok, {_AppContext, Authorization}} -> + {ok, {_AppContext2, Response}} = + oauth2:issue_token(Authorization, none), + {ok, AccessToken} = oauth2_response:access_token(Response), + {ok, Type} = oauth2_response:token_type(Response), + {ok, Expires} = oauth2_response:expires_in(Response), + {ok, VerifiedScope} = oauth2_response:scope(Response), + %oauth2_wrq:redirected_access_token_response(ReqData, + % RedirectURI, + % AccessToken, + % Type, + % Expires, + % VerifiedScope, + % State, + % Context); + {302, [{<<"Location">>, + <<RedirectURI/binary, + "?access_token=", AccessToken/binary, + "&token_type=", Type/binary, + "&expires_in=", (integer_to_binary(Expires))/binary, + "&scope=", (str:join(VerifiedScope, <<" ">>))/binary, + "&state=", State/binary>> + }], + ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])}; + {error, Error} when is_atom(Error) -> + %oauth2_wrq:redirected_error_response( + % ReqData, RedirectURI, Error, State, Context) + {302, [{<<"Location">>, + <<RedirectURI/binary, + "?error=", (atom_to_binary(Error, utf8))/binary, + "&state=", State/binary>> + }], + ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])} + end; +process(_Handlers, _Request) -> + ejabberd_web:error(not_found). + + + + +web_head() -> + [?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>}, + {<<"content">>, <<"IE=edge">>}]), + ?XA(<<"meta">>, [{<<"name">>, <<"viewport">>}, + {<<"content">>, + <<"width=device-width, initial-scale=1">>}]), + ?XC(<<"title">>, <<"Authorization request">>), + ?XC(<<"style">>, css()) + ]. + +css() -> + <<" + body { + margin: 0; + padding: 0; + + font-family: sans-serif; + color: #fff; + } + + h1 { + font-size: 3em; + color: #444; + } + + p { + line-height: 1.5em; + color: #888; + } + + a { + color: #fff; + } + a:hover, + a:active { + text-decoration: underline; + } + + em { + display: inline-block; + padding: 0 5px; + + background: #f4f4f4; + border-radius: 5px; + + font-style: normal; + font-weight: bold; + color: #444; + } + + form { + color: #444; + } + label { + display: block; + font-weight: bold; + } + + input[type=text], + input[type=password] { + margin-bottom: 1em; + padding: 0.4em; + + max-width: 330px; + width: 100%; + + border: 1px solid #c4c4c4; + border-radius: 5px; + outline: 0; + + font-size: 1.2em; + } + input[type=text]:focus, + input[type=password]:focus, + input[type=text]:active, + input[type=password]:active { + border-color: #41AFCA; + } + + input[type=submit] { + font-size: 1em; + } + + .container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + background: #424A55; + background-image: -webkit-linear-gradient(270deg, rgba(48,52,62,0) 24%, #30353e 100%); + background-image: linear-gradient(-180deg, rgba(48,52,62,0) 24%, #30353e 100%); + } + + .section { + padding: 3em; + } + .white.section { + background: #fff; + border-bottom: 4px solid #41AFCA; + } + + .white.section a { + text-decoration: none; + color: #41AFCA; + } + .white.section a:hover, + .white.section a:active { + text-decoration: underline; + } + + .container > .section { + background: #424A55; + } + + .block { + margin: 0 auto; + max-width: 900px; + width: 100%; + } +">>. + +logo() -> + <<"data:image/png;base64,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">>. + +opt_type(oauth_expire) -> + fun(I) when is_integer(I), I >= 0 -> I end; +opt_type(_) -> [oauth_expire]. diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl index 1d50bf45..db109920 100644 --- a/src/ejabberd_xmlrpc.erl +++ b/src/ejabberd_xmlrpc.erl @@ -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]); diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl new file mode 100644 index 00000000..d12af0c9 --- /dev/null +++ b/src/mod_http_api.erl @@ -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]. diff --git a/vars.config.in b/vars.config.in index 2cd5d8e9..74904724 100644 --- a/vars.config.in +++ b/vars.config.in @@ -31,6 +31,7 @@ {elixir, @elixir@}. {lager, @lager@}. {iconv, @iconv@}. +{oauth, @oauth@}. %% Version {vsn, "@PACKAGE_VERSION@"}. |