]> granicus.if.org Git - ejabberd/commitdiff
Add OAuth support (thanks to Aleksey)
authorChristophe Romain <christophe.romain@process-one.net>
Fri, 25 Sep 2015 12:53:25 +0000 (14:53 +0200)
committerChristophe Romain <christophe.romain@process-one.net>
Fri, 25 Sep 2015 13:49:07 +0000 (15:49 +0200)
13 files changed:
configure.ac
include/ejabberd_commands.hrl
rebar.config.script
src/cyrsasl.erl
src/cyrsasl_oauth.erl [new file with mode: 0644]
src/ejabberd_app.erl
src/ejabberd_commands.erl
src/ejabberd_ctl.erl
src/ejabberd_http.erl
src/ejabberd_oauth.erl [new file with mode: 0644]
src/ejabberd_xmlrpc.erl
src/mod_http_api.erl [new file with mode: 0644]
vars.config.in

index d30a490d0805440537748c2ea5025d5faacf460e..69fd3cef52d5753d4f53314d542e3c36941b2fee 100644 (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
index 3ab15ca31f5938e1a09cb384d9272ceb56f9b8bc..82ee891c3a6d19f9491acf01dbde34e47b2e2e57 100644 (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(),
index 7ec52722b85320c675329dead1460cf5337eee9b..2ecb39c25840feb2c39db4e10a7caed9576a1f46 100644 (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),
index 970da5bb9bc1d21b92f08107f8309703ff05dba4..518bb84ff5007386a8ac6601d2dbffece0d16d01 100644 (file)
@@ -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 (file)
index 0000000..fc5a9c6
--- /dev/null
@@ -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]).
index 319e494c2236b8ff02d1ed7b1a50890ad7a4f651..657785ff982ee98f2d02676e8c20c590743725db 100644 (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()]),
index a5ba98ae808a620b4bb1015199e956b734c3c602..34297e006b4e43d06e62382315bd13bdd06d045f 100644 (file)
 -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].
index 82ac6b134f9fb2bbe6b8c3e1989c99818b1d1557..8b6f1852079ac8c88f6a3a4d957fbb90688a1d62 100644 (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
index 61ce64734119250b7ccb60f0ca3f29cbd5c31bd4..4c647ab58074cf58048e5801fcfd5db93b8bbba6 100644 (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) ->
diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl
new file mode 100644 (file)
index 0000000..0295b9d
--- /dev/null
@@ -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].
index 1d50bf45b567ee17644ffbdd138e7354189a16a7..db109920cbd0f804bff258933c25bf1aa5465bd1 100644 (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]);
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
new file mode 100644 (file)
index 0000000..d12af0c
--- /dev/null
@@ -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].
index 2cd5d8e9a6fd1710d0270f4a294e8489554563c6..74904724aba6d7cf229299eefdc9135821839629 100644 (file)
@@ -31,6 +31,7 @@
 {elixir, @elixir@}.
 {lager, @lager@}.
 {iconv, @iconv@}.
+{oauth, @oauth@}.
 
 %% Version
 {vsn, "@PACKAGE_VERSION@"}.