1 |
|
%%%------------------------------------------------------------------- |
2 |
|
%%% @author ludwikbukowski |
3 |
|
%%% @copyright (C) 2016, Erlang Solutions Ltd. |
4 |
|
%%% |
5 |
|
%%% @end |
6 |
|
%%% Created : 19. Jul 2016 17:55 |
7 |
|
%%%------------------------------------------------------------------- |
8 |
|
%% @doc MongooseIM REST HTTP API for clients. |
9 |
|
%% This module implements cowboy REST callbacks and |
10 |
|
%% passes the requests on to the http api backend module. |
11 |
|
%% It provides also client authorization mechanism |
12 |
|
|
13 |
|
-module(mongoose_api_client). |
14 |
|
-author("ludwikbukowski"). |
15 |
|
-include("mongoose_api.hrl"). |
16 |
|
-include("jlib.hrl"). |
17 |
|
-include("mongoose.hrl"). |
18 |
|
|
19 |
|
-behaviour(mongoose_http_handler). |
20 |
|
|
21 |
|
%% mongoose_http_handler callbacks |
22 |
|
-export([routes/1]). |
23 |
|
|
24 |
|
-export([to_json/2, from_json/2]). |
25 |
|
|
26 |
|
%% API |
27 |
|
-export([is_authorized/2, |
28 |
|
init/2, |
29 |
|
allowed_methods/2, |
30 |
|
content_types_provided/2, |
31 |
|
content_types_accepted/2, |
32 |
|
rest_terminate/2, |
33 |
|
delete_resource/2]). |
34 |
|
|
35 |
|
-ignore_xref([allowed_methods/2, content_types_accepted/2, content_types_provided/2, |
36 |
|
cowboy_router_paths/2, delete_resource/2, from_json/2, init/2, |
37 |
|
is_authorized/2, rest_terminate/2, to_json/2]). |
38 |
|
|
39 |
|
-import(mongoose_api_common, [action_to_method/1, |
40 |
|
method_to_action/1, |
41 |
|
process_request/4, |
42 |
|
error_response/4, |
43 |
|
parse_request_body/1]). |
44 |
|
|
45 |
|
%%-------------------------------------------------------------------- |
46 |
|
%% mongoose_http_handler callbacks |
47 |
|
%%-------------------------------------------------------------------- |
48 |
|
|
49 |
|
-spec routes(mongoose_http_handler:options()) -> mongoose_http_handler:routes(). |
50 |
|
routes(#{path := BasePath}) -> |
51 |
:-( |
ejabberd_hooks:add(register_command, global, mongoose_api_common, reload_dispatches, 50), |
52 |
:-( |
ejabberd_hooks:add(unregister_command, global, mongoose_api_common, reload_dispatches, 50), |
53 |
:-( |
try |
54 |
:-( |
Commands = mongoose_commands:list(user), |
55 |
:-( |
[handler_path(BasePath, Command) || Command <- Commands] |
56 |
|
catch |
57 |
|
Class:Err:Stacktrace -> |
58 |
:-( |
?LOG_ERROR(#{what => rest_getting_command_list_failed, |
59 |
:-( |
class => Class, reason => Err, stacktrace => Stacktrace}), |
60 |
:-( |
[] |
61 |
|
end. |
62 |
|
|
63 |
|
%%-------------------------------------------------------------------- |
64 |
|
%% cowboy_rest callbacks |
65 |
|
%%-------------------------------------------------------------------- |
66 |
|
|
67 |
|
|
68 |
|
init(Req, #{command_category := CommandCategory}) -> |
69 |
:-( |
Bindings = maps:to_list(cowboy_req:bindings(Req)), |
70 |
:-( |
State = #http_api_state{allowed_methods = mongoose_api_common:get_allowed_methods(user), |
71 |
|
bindings = Bindings, |
72 |
|
command_category = CommandCategory}, |
73 |
:-( |
{cowboy_rest, Req, State}. |
74 |
|
|
75 |
|
allowed_methods(Req, #http_api_state{command_category = Name} = State) -> |
76 |
:-( |
CommandList = mongoose_commands:list(user, Name), |
77 |
:-( |
AllowedMethods = [action_to_method(mongoose_commands:action(Command)) |
78 |
:-( |
|| Command <- CommandList], |
79 |
:-( |
{AllowedMethods, Req, State}. |
80 |
|
|
81 |
|
content_types_provided(Req, State) -> |
82 |
:-( |
CTP = [{{<<"application">>, <<"json">>, '*'}, to_json}], |
83 |
:-( |
{CTP, Req, State}. |
84 |
|
|
85 |
|
content_types_accepted(Req, State) -> |
86 |
:-( |
CTA = [{{<<"application">>, <<"json">>, '*'}, from_json}], |
87 |
:-( |
{CTA, Req, State}. |
88 |
|
|
89 |
|
rest_terminate(_Req, _State) -> |
90 |
:-( |
ok. |
91 |
|
|
92 |
|
is_authorized(Req, State) -> |
93 |
:-( |
AuthDetails = cowboy_req:parse_header(<<"authorization">>, Req), |
94 |
:-( |
do_authorize(AuthDetails, Req, State). |
95 |
|
|
96 |
|
%% @doc Called for a method of type "DELETE" |
97 |
|
delete_resource(Req, #http_api_state{command_category = Category, |
98 |
|
command_subcategory = SubCategory, |
99 |
|
bindings = B} = State) -> |
100 |
:-( |
Arity = length(B), |
101 |
:-( |
Cmds = mongoose_commands:list(user, Category, method_to_action(<<"DELETE">>), SubCategory), |
102 |
:-( |
[Command] = [C || C <- Cmds, arity(C) == Arity], |
103 |
:-( |
mongoose_api_common:process_request(<<"DELETE">>, Command, Req, State). |
104 |
|
|
105 |
|
%%-------------------------------------------------------------------- |
106 |
|
%% internal funs |
107 |
|
%%-------------------------------------------------------------------- |
108 |
|
|
109 |
|
%% @doc Called for a method of type "GET" |
110 |
|
to_json(Req, #http_api_state{command_category = Category, |
111 |
|
command_subcategory = SubCategory, |
112 |
|
bindings = B} = State) -> |
113 |
:-( |
Arity = length(B), |
114 |
:-( |
Cmds = mongoose_commands:list(user, Category, method_to_action(<<"GET">>), SubCategory), |
115 |
:-( |
[Command] = [C || C <- Cmds, arity(C) == Arity], |
116 |
:-( |
mongoose_api_common:process_request(<<"GET">>, Command, Req, State). |
117 |
|
|
118 |
|
|
119 |
|
%% @doc Called for a method of type "POST" and "PUT" |
120 |
|
from_json(Req, #http_api_state{command_category = Category, |
121 |
|
command_subcategory = SubCategory, |
122 |
|
bindings = B} = State) -> |
123 |
:-( |
Method = cowboy_req:method(Req), |
124 |
:-( |
case parse_request_body(Req) of |
125 |
|
{error, _R}-> |
126 |
:-( |
error_response(bad_request, ?BODY_MALFORMED, Req, State); |
127 |
|
{Params, _} -> |
128 |
:-( |
Arity = length(B) + length(Params), |
129 |
:-( |
Cmds = mongoose_commands:list(user, Category, method_to_action(Method), SubCategory), |
130 |
:-( |
case [C || C <- Cmds, arity(C) == Arity] of |
131 |
|
[Command] -> |
132 |
:-( |
process_request(Method, Command, {Params, Req}, State); |
133 |
|
[] -> |
134 |
:-( |
error_response(not_found, ?ARGS_LEN_ERROR, Req, State) |
135 |
|
end |
136 |
|
end. |
137 |
|
|
138 |
|
arity(C) -> |
139 |
|
% we don't have caller in bindings (we know it from authorisation), |
140 |
|
% so it doesn't count when checking arity |
141 |
:-( |
Args = mongoose_commands:args(C), |
142 |
:-( |
length([N || {N, _} <- Args, N =/= caller]). |
143 |
|
|
144 |
|
do_authorize({basic, User, Password}, Req, State) -> |
145 |
:-( |
case jid:from_binary(User) of |
146 |
|
error -> |
147 |
:-( |
make_unauthorized_response(Req, State); |
148 |
|
JID -> |
149 |
:-( |
do_check_password(JID, Password, Req, State) |
150 |
|
end; |
151 |
|
do_authorize(_, Req, State) -> |
152 |
:-( |
make_unauthorized_response(Req, State). |
153 |
|
|
154 |
|
do_check_password(#jid{} = JID, Password, Req, State) -> |
155 |
:-( |
case ejabberd_auth:check_password(JID, Password) of |
156 |
|
true -> |
157 |
:-( |
{true, Req, State#http_api_state{entity = jid:to_binary(JID)}}; |
158 |
|
_ -> |
159 |
:-( |
make_unauthorized_response(Req, State) |
160 |
|
end. |
161 |
|
|
162 |
|
make_unauthorized_response(Req, State) -> |
163 |
:-( |
{{false, <<"Basic realm=\"mongooseim\"">>}, Req, State}. |
164 |
|
|
165 |
|
-spec handler_path(ejabberd_cowboy:path(), mongoose_commands:t()) -> ejabberd_cowboy:route(). |
166 |
|
handler_path(Base, Command) -> |
167 |
:-( |
{[Base, mongoose_api_common:create_user_url_path(Command)], |
168 |
|
?MODULE, #{command_category => mongoose_commands:category(Command)}}. |
169 |
|
|