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