1 |
|
%%%------------------------------------------------------------------- |
2 |
|
%%% @author ludwikbukowski |
3 |
|
%%% @copyright (C) 2016, Erlang Solutions Ltd. |
4 |
|
%%% Created : 05. Jul 2016 12:59 |
5 |
|
%%%------------------------------------------------------------------- |
6 |
|
|
7 |
|
%% @doc MongooseIM REST HTTP API for administration. |
8 |
|
%% This module implements cowboy REST callbacks and |
9 |
|
%% passes the requests on to the http api backend module. |
10 |
|
%% @end |
11 |
|
-module(mongoose_api_admin). |
12 |
|
-author("ludwikbukowski"). |
13 |
|
-behaviour(cowboy_rest). |
14 |
|
|
15 |
|
%% ejabberd_cowboy exports |
16 |
|
-export([cowboy_router_paths/2]). |
17 |
|
|
18 |
|
%% cowboy_rest exports |
19 |
|
-export([allowed_methods/2, |
20 |
|
content_types_provided/2, |
21 |
|
terminate/3, |
22 |
|
init/2, |
23 |
|
options/2, |
24 |
|
content_types_accepted/2, |
25 |
|
delete_resource/2, |
26 |
|
is_authorized/2]). |
27 |
|
%% local callbacks |
28 |
|
-export([to_json/2, from_json/2]). |
29 |
|
|
30 |
|
-ignore_xref([cowboy_router_paths/2, from_json/2, to_json/2]). |
31 |
|
|
32 |
|
-include("mongoose_api.hrl"). |
33 |
|
-include("mongoose.hrl"). |
34 |
|
|
35 |
|
-import(mongoose_api_common, [error_response/3, |
36 |
|
error_response/4, |
37 |
|
action_to_method/1, |
38 |
|
method_to_action/1, |
39 |
|
error_code/1, |
40 |
|
process_request/4, |
41 |
|
parse_request_body/1]). |
42 |
|
|
43 |
|
-type credentials() :: {Username :: binary(), Password :: binary()} | any. |
44 |
|
|
45 |
|
%%-------------------------------------------------------------------- |
46 |
|
%% ejabberd_cowboy callbacks |
47 |
|
%%-------------------------------------------------------------------- |
48 |
|
|
49 |
|
%% @doc This is implementation of ejabberd_cowboy callback. |
50 |
|
%% Returns list of all available http paths. |
51 |
|
-spec cowboy_router_paths(ejabberd_cowboy:path(), ejabberd_cowboy:options()) -> |
52 |
|
ejabberd_cowboy:implemented_result(). |
53 |
|
cowboy_router_paths(Base, Opts) -> |
54 |
122 |
ejabberd_hooks:add(register_command, global, mongoose_api_common, reload_dispatches, 50), |
55 |
122 |
ejabberd_hooks:add(unregister_command, global, mongoose_api_common, reload_dispatches, 50), |
56 |
122 |
try |
57 |
122 |
Commands = mongoose_commands:list(admin), |
58 |
122 |
[handler_path(Base, Command, Opts) || Command <- Commands] |
59 |
|
catch |
60 |
|
Class:Err:StackTrace -> |
61 |
:-( |
?LOG_ERROR(#{what => getting_command_list_error, |
62 |
:-( |
class => Class, reason => Err, stacktrace => StackTrace}), |
63 |
:-( |
[] |
64 |
|
end. |
65 |
|
|
66 |
|
%%-------------------------------------------------------------------- |
67 |
|
%% cowboy_rest callbacks |
68 |
|
%%-------------------------------------------------------------------- |
69 |
|
|
70 |
|
init(Req, Opts) -> |
71 |
103 |
Bindings = maps:to_list(cowboy_req:bindings(Req)), |
72 |
103 |
CommandCategory = proplists:get_value(command_category, Opts), |
73 |
103 |
CommandSubCategory = proplists:get_value(command_subcategory, Opts), |
74 |
103 |
Auth = proplists:get_value(auth, Opts, any), |
75 |
103 |
State = #http_api_state{allowed_methods = mongoose_api_common:get_allowed_methods(admin), |
76 |
|
bindings = Bindings, |
77 |
|
command_category = CommandCategory, |
78 |
|
command_subcategory = CommandSubCategory, |
79 |
|
auth = Auth}, |
80 |
103 |
{cowboy_rest, Req, State}. |
81 |
|
|
82 |
|
options(Req, State) -> |
83 |
:-( |
Req1 = set_cors_headers(Req), |
84 |
:-( |
{ok, Req1, State}. |
85 |
|
|
86 |
|
set_cors_headers(Req) -> |
87 |
:-( |
Req1 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Methods">>, |
88 |
|
<<"GET, OPTIONS, PUT, POST, DELETE">>, Req), |
89 |
:-( |
Req2 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Origin">>, |
90 |
|
<<"*">>, Req1), |
91 |
:-( |
cowboy_req:set_resp_header(<<"Access-Control-Allow-Headers">>, |
92 |
|
<<"Content-Type">>, Req2). |
93 |
|
|
94 |
|
allowed_methods(Req, #http_api_state{command_category = Name} = State) -> |
95 |
103 |
CommandList = mongoose_commands:list(admin, Name), |
96 |
103 |
AllowedMethods = [action_to_method(mongoose_commands:action(Command)) |
97 |
103 |
|| Command <- CommandList], |
98 |
103 |
{[<<"OPTIONS">> | AllowedMethods], Req, State}. |
99 |
|
|
100 |
|
content_types_provided(Req, State) -> |
101 |
102 |
CTP = [{{<<"application">>, <<"json">>, '*'}, to_json}], |
102 |
102 |
{CTP, Req, State}. |
103 |
|
|
104 |
|
content_types_accepted(Req, State) -> |
105 |
58 |
CTA = [{{<<"application">>, <<"json">>, '*'}, from_json}], |
106 |
58 |
{CTA, Req, State}. |
107 |
|
|
108 |
|
terminate(_Reason, _Req, _State) -> |
109 |
103 |
ok. |
110 |
|
|
111 |
|
%% @doc Called for a method of type "DELETE" |
112 |
|
delete_resource(Req, #http_api_state{command_category = Category, |
113 |
|
command_subcategory = SubCategory, |
114 |
|
bindings = B} = State) -> |
115 |
11 |
Arity = length(B), |
116 |
11 |
Cmds = mongoose_commands:list(admin, Category, method_to_action(<<"DELETE">>), SubCategory), |
117 |
11 |
[Command] = [C || C <- Cmds, mongoose_commands:arity(C) == Arity], |
118 |
11 |
process_request(<<"DELETE">>, Command, Req, State). |
119 |
|
|
120 |
|
|
121 |
|
%%-------------------------------------------------------------------- |
122 |
|
%% Authorization |
123 |
|
%%-------------------------------------------------------------------- |
124 |
|
|
125 |
|
% @doc Cowboy callback |
126 |
|
is_authorized(Req, State) -> |
127 |
103 |
ControlCreds = get_control_creds(State), |
128 |
103 |
AuthDetails = mongoose_api_common:get_auth_details(Req), |
129 |
103 |
case authorize(ControlCreds, AuthDetails) of |
130 |
|
true -> |
131 |
102 |
{true, Req, State}; |
132 |
|
false -> |
133 |
1 |
mongoose_api_common:make_unauthorized_response(Req, State) |
134 |
|
end. |
135 |
|
|
136 |
|
-spec authorize(credentials(), {AuthMethod :: atom(), |
137 |
|
Username :: binary(), |
138 |
|
Password :: binary()}) -> boolean(). |
139 |
101 |
authorize(any, _) -> true; |
140 |
:-( |
authorize(_, undefined) -> false; |
141 |
|
authorize(ControlCreds, {AuthMethod, User, Password}) -> |
142 |
2 |
compare_creds(ControlCreds, {User, Password}) andalso |
143 |
1 |
mongoose_api_common:is_known_auth_method(AuthMethod). |
144 |
|
|
145 |
|
% @doc Checks if credentials are the same (if control creds are 'any' |
146 |
|
% it is equal to everything). |
147 |
|
-spec compare_creds(credentials(), credentials() | undefined) -> boolean(). |
148 |
1 |
compare_creds({User, Pass}, {User, Pass}) -> true; |
149 |
1 |
compare_creds(_, _) -> false. |
150 |
|
|
151 |
|
get_control_creds(#http_api_state{auth = Creds}) -> |
152 |
103 |
Creds. |
153 |
|
|
154 |
|
%%-------------------------------------------------------------------- |
155 |
|
%% Internal funs |
156 |
|
%%-------------------------------------------------------------------- |
157 |
|
|
158 |
|
%% @doc Called for a method of type "GET" |
159 |
|
to_json(Req, #http_api_state{command_category = Category, |
160 |
|
command_subcategory = SubCategory, |
161 |
|
bindings = B} = State) -> |
162 |
33 |
Cmds = mongoose_commands:list(admin, Category, method_to_action(<<"GET">>), SubCategory), |
163 |
33 |
Arity = length(B), |
164 |
33 |
case [C || C <- Cmds, mongoose_commands:arity(C) == Arity] of |
165 |
|
[Command] -> |
166 |
32 |
process_request(<<"GET">>, Command, Req, State); |
167 |
|
[] -> |
168 |
1 |
error_response(not_found, ?ARGS_LEN_ERROR, Req, State) |
169 |
|
end. |
170 |
|
|
171 |
|
%% @doc Called for a method of type "POST" and "PUT" |
172 |
|
from_json(Req, #http_api_state{command_category = Category, |
173 |
|
command_subcategory = SubCategory, |
174 |
|
bindings = B} = State) -> |
175 |
58 |
case parse_request_body(Req) of |
176 |
|
{error, _R}-> |
177 |
:-( |
error_response(bad_request, ?BODY_MALFORMED, Req, State); |
178 |
|
{Params, _} -> |
179 |
58 |
Method = cowboy_req:method(Req), |
180 |
58 |
Cmds = mongoose_commands:list(admin, Category, method_to_action(Method), SubCategory), |
181 |
58 |
QVals = cowboy_req:parse_qs(Req), |
182 |
58 |
Arity = length(B) + length(Params) + length(QVals), |
183 |
58 |
case [C || C <- Cmds, mongoose_commands:arity(C) == Arity] of |
184 |
|
[Command] -> |
185 |
58 |
process_request(Method, Command, {Params, Req}, State); |
186 |
|
[] -> |
187 |
:-( |
error_response(not_found, ?ARGS_LEN_ERROR, Req, State) |
188 |
|
end |
189 |
|
end. |
190 |
|
|
191 |
|
-spec handler_path(ejabberd_cowboy:path(), mongoose_commands:t(), [{atom(), term()}]) -> |
192 |
|
ejabberd_cowboy:route(). |
193 |
|
handler_path(Base, Command, ExtraOpts) -> |
194 |
3009 |
{[Base, mongoose_api_common:create_admin_url_path(Command)], |
195 |
|
?MODULE, [{command_category, mongoose_commands:category(Command)}, |
196 |
|
{command_subcategory, mongoose_commands:subcategory(Command)} | ExtraOpts]}. |
197 |
|
|
198 |
|
|